diff --git a/.eslintrc.json b/.eslintrc.json index 3ad6a748650..9b8f2c61094 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -487,6 +487,14 @@ { "when": "hasBrowser", "pattern": "vs/workbench/workbench.web.main" + }, + { + "when": "hasBrowser", + "pattern": "vs/workbench/~" + }, + { + "when": "hasBrowser", + "pattern": "vs/workbench/services/*/~" } ] }, diff --git a/.github/workflows/basic.yml b/.github/workflows/basic.yml index 8a0a39315bb..12e802e9c74 100644 --- a/.github/workflows/basic.yml +++ b/.github/workflows/basic.yml @@ -1,12 +1,14 @@ name: Basic checks -on: - push: - branches: - - main - pull_request: - branches: - - main +on: workflow_dispatch + +# on: +# push: +# branches: +# - main +# pull_request: +# branches: +# - main jobs: main: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84f92eef92b..d505e2b9129 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,7 +69,7 @@ jobs: 7z.exe a .build/node_modules_cache/cache.7z -mx3 `@.build/node_modules_list.txt - name: Compile and Download - run: DEBUG=pw:install yarn npm-run-all --max_old_space_size=4095 -lp compile "electron x64" playwright-install download-builtin-extensions + run: yarn npm-run-all --max_old_space_size=4095 -lp compile "electron x64" playwright-install download-builtin-extensions - name: Compile Integration Tests run: yarn --cwd test/integration/browser compile @@ -145,7 +145,7 @@ jobs: run: yarn --frozen-lockfile --network-timeout 180000 - name: Compile and Download - run: DEBUG=pw:install yarn npm-run-all --max_old_space_size=4095 -lp compile "electron x64" playwright-install download-builtin-extensions + run: yarn npm-run-all --max_old_space_size=4095 -lp compile "electron x64" playwright-install download-builtin-extensions - name: Compile Integration Tests run: yarn --cwd test/integration/browser compile @@ -216,7 +216,7 @@ jobs: run: yarn --frozen-lockfile --network-timeout 180000 - name: Compile and Download - run: DEBUG=pw:install yarn npm-run-all --max_old_space_size=4095 -lp compile "electron x64" playwright-install download-builtin-extensions + run: yarn npm-run-all --max_old_space_size=4095 -lp compile "electron x64" playwright-install download-builtin-extensions - name: Compile Integration Tests run: yarn --cwd test/integration/browser compile @@ -289,7 +289,7 @@ jobs: run: yarn --frozen-lockfile --network-timeout 180000 - name: Download Playwright - run: DEBUG=pw:install yarn playwright-install + run: yarn playwright-install - name: Run Hygiene Checks run: yarn gulp hygiene diff --git a/.github/workflows/monaco-editor.yml b/.github/workflows/monaco-editor.yml index d6bc1a55e3e..e52713a3e99 100644 --- a/.github/workflows/monaco-editor.yml +++ b/.github/workflows/monaco-editor.yml @@ -53,7 +53,7 @@ jobs: run: yarn --frozen-lockfile --network-timeout 180000 - name: Download Playwright - run: DEBUG=pw:install yarn playwright-install + run: yarn playwright-install - name: Run Monaco Editor Checks run: yarn monaco-compile-check diff --git a/.vscode/launch.json b/.vscode/launch.json index b685af9c9be..ab98af7d3d2 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -29,7 +29,7 @@ "name": "Attach to Shared Process", "timeout": 30000, "port": 9222, - "urlFilter": "*sharedProcess.html*", + "urlFilter": "*sharedProcess*.html*", "presentation": { "hidden": true } @@ -236,7 +236,7 @@ "VSCODE_SKIP_PRELAUNCH": "1" }, "cleanUp": "wholeBrowser", - "urlFilter": "*workbench.html*", + "urlFilter": "*workbench*.html*", "runtimeArgs": [ "--inspect-brk=5875", "--no-cached-data", diff --git a/.vscode/settings.json b/.vscode/settings.json index 30bf48efe64..184042b4f2e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,7 @@ ".build": true, ".profile-oss": true, "**/.DS_Store": true, + "cli/target": true, "build/**/*.js": { "when": "$(basename).ts" } @@ -86,6 +87,11 @@ "editor.defaultFormatter": "vscode.typescript-language-features", "editor.formatOnSave": true }, + "[rust]": { + "editor.defaultFormatter": "rust-lang.rust-analyzer", + "editor.formatOnSave": true, + "editor.insertSpaces": true + }, "typescript.tsc.autoDetect": "off", "testing.autoRun.mode": "rerun", "conventionalCommits.scopes": [ diff --git a/build/azure-pipelines/common/installPlaywright.js b/build/azure-pipelines/common/installPlaywright.js index af4bd5fb54c..dbc5835243f 100644 --- a/build/azure-pipelines/common/installPlaywright.js +++ b/build/azure-pipelines/common/installPlaywright.js @@ -3,10 +3,9 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -const retry_1 = require("./retry"); +process.env.DEBUG = 'pw:install'; // enable logging for this (https://github.com/microsoft/playwright/issues/17394) const { installDefaultBrowsersForNpmInstall } = require('playwright-core/lib/server'); async function install() { - await (0, retry_1.retry)(() => installDefaultBrowsersForNpmInstall()); + await installDefaultBrowsersForNpmInstall(); } install(); diff --git a/build/azure-pipelines/common/installPlaywright.ts b/build/azure-pipelines/common/installPlaywright.ts index 5d837a55413..742b6e0e399 100644 --- a/build/azure-pipelines/common/installPlaywright.ts +++ b/build/azure-pipelines/common/installPlaywright.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { retry } from './retry'; +process.env.DEBUG='pw:install'; // enable logging for this (https://github.com/microsoft/playwright/issues/17394) + const { installDefaultBrowsersForNpmInstall } = require('playwright-core/lib/server'); async function install() { - await retry(() => installDefaultBrowsersForNpmInstall()); + await installDefaultBrowsersForNpmInstall(); } install(); diff --git a/build/azure-pipelines/darwin/helper-plugin-entitlements.plist b/build/azure-pipelines/darwin/helper-plugin-entitlements.plist new file mode 100644 index 00000000000..1cc1a152c74 --- /dev/null +++ b/build/azure-pipelines/darwin/helper-plugin-entitlements.plist @@ -0,0 +1,14 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.cs.disable-library-validation + + + diff --git a/build/azure-pipelines/darwin/product-build-darwin-test.yml b/build/azure-pipelines/darwin/product-build-darwin-test.yml index db9fd21edc5..29c7bf90925 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-test.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-test.yml @@ -12,9 +12,7 @@ steps: - script: | set -e VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ - DEBUG=pw:install yarn npm-run-all -lp "electron $(VSCODE_ARCH)" "playwright-install" - timeoutInMinutes: 5 - retryCountOnTaskFailure: 3 + yarn npm-run-all -lp "electron $(VSCODE_ARCH)" "playwright-install" displayName: Download Electron and Playwright - ${{ if eq(parameters.VSCODE_RUN_UNIT_TESTS, true) }}: diff --git a/build/azure-pipelines/linux/product-build-linux-client-test.yml b/build/azure-pipelines/linux/product-build-linux-client-test.yml index 2dd36037338..36495873d96 100644 --- a/build/azure-pipelines/linux/product-build-linux-client-test.yml +++ b/build/azure-pipelines/linux/product-build-linux-client-test.yml @@ -12,9 +12,7 @@ steps: - script: | set -e VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ - DEBUG=pw:install yarn npm-run-all -lp "electron $(VSCODE_ARCH)" "playwright-install" - timeoutInMinutes: 5 - retryCountOnTaskFailure: 3 + yarn npm-run-all -lp "electron $(VSCODE_ARCH)" "playwright-install" displayName: Download Electron and Playwright - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index c7737600f38..b0631e607dc 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -94,11 +94,18 @@ steps: - script: | set -e - yarn npm-run-all -lp core-ci extensions-ci hygiene eslint valid-layers-check + yarn npm-run-all -lp core-ci extensions-ci hygiene eslint valid-layers-check vscode-dts-compile-check tsec-compile-check env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Compile & Hygiene + - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: + - script: | + set -e + yarn --cwd build compile + ./.github/workflows/check-clean-git-state.sh + displayName: Check /build/ folder + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - script: | set -e diff --git a/build/azure-pipelines/win32/product-build-win32-test.yml b/build/azure-pipelines/win32/product-build-win32-test.yml index b3196c537a4..9a17a88bca7 100644 --- a/build/azure-pipelines/win32/product-build-win32-test.yml +++ b/build/azure-pipelines/win32/product-build-win32-test.yml @@ -13,9 +13,7 @@ steps: . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" $env:VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" - exec { $env:DEBUG = "pw:install"; yarn npm-run-all -lp "electron $(VSCODE_ARCH)" "playwright-install" } - timeoutInMinutes: 5 - retryCountOnTaskFailure: 3 + exec { yarn npm-run-all -lp "electron $(VSCODE_ARCH)" "playwright-install" } displayName: Download Electron and Playwright - ${{ if eq(parameters.VSCODE_RUN_UNIT_TESTS, true) }}: diff --git a/build/darwin/sign.js b/build/darwin/sign.js index 8aabb83c0f9..ef6bef395e4 100644 --- a/build/darwin/sign.js +++ b/build/darwin/sign.js @@ -26,6 +26,7 @@ async function main() { const helperAppBaseName = product.nameShort; const gpuHelperAppName = helperAppBaseName + ' Helper (GPU).app'; const rendererHelperAppName = helperAppBaseName + ' Helper (Renderer).app'; + const pluginHelperAppName = helperAppBaseName + ' Helper (Plugin).app'; const infoPlistPath = path.resolve(appRoot, appName, 'Contents', 'Info.plist'); const defaultOpts = { app: path.join(appRoot, appName), @@ -45,7 +46,8 @@ async function main() { // TODO(deepak1556): Incorrectly declared type in electron-osx-sign ignore: (filePath) => { return filePath.includes(gpuHelperAppName) || - filePath.includes(rendererHelperAppName); + filePath.includes(rendererHelperAppName) || + filePath.includes(pluginHelperAppName); } }; const gpuHelperOpts = { @@ -60,6 +62,12 @@ async function main() { entitlements: path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-renderer-entitlements.plist'), 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-renderer-entitlements.plist'), }; + const pluginHelperOpts = { + ...defaultOpts, + app: path.join(appFrameworkPath, pluginHelperAppName), + entitlements: path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-plugin-entitlements.plist'), + 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-plugin-entitlements.plist'), + }; // Only overwrite plist entries for x64 and arm64 builds, // universal will get its copy from the x64 build. if (arch !== 'universal') { @@ -87,6 +95,7 @@ async function main() { } await codesign.signAsync(gpuHelperOpts); await codesign.signAsync(rendererHelperOpts); + await codesign.signAsync(pluginHelperOpts); await codesign.signAsync(appOpts); } if (require.main === module) { diff --git a/build/darwin/sign.ts b/build/darwin/sign.ts index 1de593fbdd4..7490d45eaed 100644 --- a/build/darwin/sign.ts +++ b/build/darwin/sign.ts @@ -29,6 +29,7 @@ async function main(): Promise { const helperAppBaseName = product.nameShort; const gpuHelperAppName = helperAppBaseName + ' Helper (GPU).app'; const rendererHelperAppName = helperAppBaseName + ' Helper (Renderer).app'; + const pluginHelperAppName = helperAppBaseName + ' Helper (Plugin).app'; const infoPlistPath = path.resolve(appRoot, appName, 'Contents', 'Info.plist'); const defaultOpts: codesign.SignOptions = { @@ -50,7 +51,8 @@ async function main(): Promise { // TODO(deepak1556): Incorrectly declared type in electron-osx-sign ignore: (filePath: string) => { return filePath.includes(gpuHelperAppName) || - filePath.includes(rendererHelperAppName); + filePath.includes(rendererHelperAppName) || + filePath.includes(pluginHelperAppName); } }; @@ -68,6 +70,13 @@ async function main(): Promise { 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-renderer-entitlements.plist'), }; + const pluginHelperOpts: codesign.SignOptions = { + ...defaultOpts, + app: path.join(appFrameworkPath, pluginHelperAppName), + entitlements: path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-plugin-entitlements.plist'), + 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-plugin-entitlements.plist'), + }; + // Only overwrite plist entries for x64 and arm64 builds, // universal will get its copy from the x64 build. if (arch !== 'universal') { @@ -96,6 +105,7 @@ async function main(): Promise { await codesign.signAsync(gpuHelperOpts); await codesign.signAsync(rendererHelperOpts); + await codesign.signAsync(pluginHelperOpts); await codesign.signAsync(appOpts as any); } diff --git a/build/gulpfile.compile.js b/build/gulpfile.compile.js index c3049784fe4..5b6ce2d70be 100644 --- a/build/gulpfile.compile.js +++ b/build/gulpfile.compile.js @@ -9,13 +9,15 @@ const gulp = require('gulp'); const util = require('./lib/util'); const task = require('./lib/task'); const compilation = require('./lib/compilation'); +const optimize = require('./lib/optimize'); // Full compile, including nls and inline sources in sourcemaps, for build const compileBuildTask = task.define('compile-build', task.series( util.rimraf('out-build'), util.buildWebNodePaths('out-build'), - compilation.compileTask('src', 'out-build', true) + compilation.compileTask('src', 'out-build', true), + optimize.optimizeLoaderTask('out-build', 'out-build', true) ) ); gulp.task(compileBuildTask); diff --git a/build/gulpfile.editor.js b/build/gulpfile.editor.js index 8e7ff5e3af3..94724bb410d 100644 --- a/build/gulpfile.editor.js +++ b/build/gulpfile.editor.js @@ -7,7 +7,7 @@ const gulp = require('gulp'); const path = require('path'); const util = require('./lib/util'); const task = require('./lib/task'); -const common = require('./lib/optimize'); +const optimize = require('./lib/optimize'); const es = require('event-stream'); const File = require('vinyl'); const i18n = require('./lib/i18n'); @@ -86,26 +86,29 @@ const extractEditorSrcTask = task.define('extract-editor-src', () => { const compileEditorAMDTask = task.define('compile-editor-amd', compilation.compileTask('out-editor-src', 'out-editor-build', true)); -const optimizeEditorAMDTask = task.define('optimize-editor-amd', common.optimizeTask({ - src: 'out-editor-build', - entryPoints: editorEntryPoints, - resources: editorResources, - loaderConfig: { - paths: { - 'vs': 'out-editor-build/vs', - 'vs/css': 'out-editor-build/vs/css.build', - 'vs/nls': 'out-editor-build/vs/nls.build', - 'vscode': 'empty:' +const optimizeEditorAMDTask = task.define('optimize-editor-amd', optimize.optimizeTask( + { + out: 'out-editor', + amd: { + src: 'out-editor-build', + entryPoints: editorEntryPoints, + resources: editorResources, + loaderConfig: { + paths: { + 'vs': 'out-editor-build/vs', + 'vs/css': 'out-editor-build/vs/css.build', + 'vs/nls': 'out-editor-build/vs/nls.build', + 'vscode': 'empty:' + } + }, + header: BUNDLED_FILE_HEADER, + bundleInfo: true, + languages } - }, - bundleLoader: false, - header: BUNDLED_FILE_HEADER, - bundleInfo: true, - out: 'out-editor', - languages: languages -})); + } +)); -const minifyEditorAMDTask = task.define('minify-editor-amd', common.minifyTask('out-editor')); +const minifyEditorAMDTask = task.define('minify-editor-amd', optimize.minifyTask('out-editor')); const createESMSourcesAndResourcesTask = task.define('extract-editor-esm', () => { standalone.createESMSourcesAndResources2({ diff --git a/build/gulpfile.extensions.js b/build/gulpfile.extensions.js index 1e3a9e4daa3..4023b3860e9 100644 --- a/build/gulpfile.extensions.js +++ b/build/gulpfile.extensions.js @@ -20,7 +20,6 @@ const root = path.dirname(__dirname); const commit = util.getVersion(root); const plumber = require('gulp-plumber'); const ext = require('./lib/extensions'); -const product = require('../product.json'); const extensionsPath = path.join(path.dirname(__dirname), 'extensions'); diff --git a/build/gulpfile.reh.js b/build/gulpfile.reh.js index a07c55232de..980f647c854 100644 --- a/build/gulpfile.reh.js +++ b/build/gulpfile.reh.js @@ -10,7 +10,7 @@ const path = require('path'); const es = require('event-stream'); const util = require('./lib/util'); const task = require('./lib/task'); -const common = require('./lib/optimize'); +const optimize = require('./lib/optimize'); const product = require('../product.json'); const rename = require('gulp-rename'); const replace = require('gulp-replace'); @@ -58,15 +58,10 @@ const serverResources = [ 'out-build/bootstrap-fork.js', 'out-build/bootstrap-amd.js', 'out-build/bootstrap-node.js', - 'out-build/paths.js', // Performance 'out-build/vs/base/common/performance.js', - // main entry points - 'out-build/server-cli.js', - 'out-build/server-main.js', - // Watcher 'out-build/vs/platform/files/**/*.exe', 'out-build/vs/platform/files/**/*.md', @@ -254,7 +249,7 @@ function packageTask(type, platform, arch, sourceFolderName, destinationFolderNa const date = new Date().toISOString(); const productJsonStream = gulp.src(['product.json'], { base: '.' }) - .pipe(json({ commit, date })); + .pipe(json({ commit, date, version })); const license = gulp.src(['remote/LICENSE'], { base: 'remote', allowEmpty: true }); @@ -357,23 +352,42 @@ function tweakProductForServerWeb(product) { ['reh', 'reh-web'].forEach(type => { const optimizeTask = task.define(`optimize-vscode-${type}`, task.series( util.rimraf(`out-vscode-${type}`), - common.optimizeTask({ - src: 'out-build', - entryPoints: _.flatten(type === 'reh' ? serverEntryPoints : serverWithWebEntryPoints), - otherSources: [], - resources: type === 'reh' ? serverResources : serverWithWebResources, - loaderConfig: common.loaderConfig(), - out: `out-vscode-${type}`, - inlineAmdImages: true, - bundleInfo: undefined, - fileContentMapper: createVSCodeWebFileContentMapper('.build/extensions', type === 'reh-web' ? tweakProductForServerWeb(product) : product) - }) + optimize.optimizeTask( + { + out: `out-vscode-${type}`, + amd: { + src: 'out-build', + entryPoints: _.flatten(type === 'reh' ? serverEntryPoints : serverWithWebEntryPoints), + otherSources: [], + resources: type === 'reh' ? serverResources : serverWithWebResources, + loaderConfig: optimize.loaderConfig(), + inlineAmdImages: true, + bundleInfo: undefined, + fileContentMapper: createVSCodeWebFileContentMapper('.build/extensions', type === 'reh-web' ? tweakProductForServerWeb(product) : product) + }, + commonJS: { + src: 'out-build', + entryPoints: [ + 'out-build/server-main.js', + 'out-build/server-cli.js' + ], + platform: 'node', + external: [ + 'minimist', + // TODO: we cannot inline `product.json` because + // it is being changed during build time at a later + // point in time (such as `checksums`) + '../product.json' + ] + } + } + ) )); const minifyTask = task.define(`minify-vscode-${type}`, task.series( optimizeTask, util.rimraf(`out-vscode-${type}-min`), - common.minifyTask(`out-vscode-${type}`, `https://ticino.blob.core.windows.net/sourcemaps/${commit}/core`) + optimize.minifyTask(`out-vscode-${type}`, `https://ticino.blob.core.windows.net/sourcemaps/${commit}/core`) )); gulp.task(minifyTask); diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index d371e6f3c25..6947d1ee1ed 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -20,7 +20,7 @@ const _ = require('underscore'); const util = require('./lib/util'); const task = require('./lib/task'); const buildfile = require('../src/buildfile'); -const common = require('./lib/optimize'); +const optimize = require('./lib/optimize'); const root = path.dirname(__dirname); const commit = util.getVersion(root); const packageJson = require('../package.json'); @@ -52,8 +52,6 @@ const vscodeEntryPoints = _.flatten([ ]); const vscodeResources = [ - 'out-build/main.js', - 'out-build/cli.js', 'out-build/bootstrap.js', 'out-build/bootstrap-fork.js', 'out-build/bootstrap-amd.js', @@ -63,12 +61,9 @@ const vscodeResources = [ '!out-build/vs/code/browser/**/*.html', '!out-build/vs/editor/standalone/**/*.svg', 'out-build/vs/base/common/performance.js', - 'out-build/vs/base/common/stripComments.js', - 'out-build/vs/base/node/languagePacks.js', 'out-build/vs/base/node/{stdForkStart.js,terminateProcess.sh,cpuUsage.sh,ps.sh}', 'out-build/vs/base/browser/ui/codicons/codicon/**', 'out-build/vs/base/parts/sandbox/electron-browser/preload.js', - 'out-build/vs/platform/environment/node/userDataPath.js', 'out-build/vs/workbench/browser/media/*-theme.css', 'out-build/vs/workbench/contrib/debug/**/*.json', 'out-build/vs/workbench/contrib/externalTerminal/**/*.scpt', @@ -81,23 +76,58 @@ const vscodeResources = [ 'out-build/vs/workbench/contrib/tasks/**/*.json', 'out-build/vs/platform/files/**/*.exe', 'out-build/vs/platform/files/**/*.md', - 'out-build/vs/code/electron-sandbox/workbench/**', - 'out-build/vs/code/electron-browser/sharedProcess/sharedProcess.js', - 'out-build/vs/code/electron-sandbox/issue/issueReporter.js', - 'out-build/vs/code/electron-sandbox/processExplorer/processExplorer.js', '!**/test/**' ]; +// Do not change the order of these files! They will +// be inlined into the target window file in this order +// and they depend on each other in this way. +const windowBootstrapFiles = [ + 'out-build/bootstrap.js', + 'out-build/vs/loader.js', + 'out-build/bootstrap-window.js' +]; + const optimizeVSCodeTask = task.define('optimize-vscode', task.series( util.rimraf('out-vscode'), - common.optimizeTask({ - src: 'out-build', - entryPoints: vscodeEntryPoints, - resources: vscodeResources, - loaderConfig: common.loaderConfig(), - out: 'out-vscode', - bundleInfo: undefined - }) + // Optimize: bundles source files automatically based on + // AMD and CommonJS import statements based on the passed + // in entry points. In addition, concat window related + // bootstrap files into a single file. + optimize.optimizeTask( + { + out: 'out-vscode', + amd: { + src: 'out-build', + entryPoints: vscodeEntryPoints, + resources: vscodeResources, + loaderConfig: optimize.loaderConfig(), + bundleInfo: undefined + }, + commonJS: { + src: 'out-build', + entryPoints: [ + 'out-build/main.js', + 'out-build/cli.js' + ], + platform: 'node', + external: [ + 'electron', + 'minimist', + // TODO: we cannot inline `product.json` because + // it is being changed during build time at a later + // point in time (such as `checksums`) + '../product.json' + ] + }, + manual: [ + { src: [...windowBootstrapFiles, 'out-build/vs/code/electron-sandbox/workbench/workbench.js'], out: 'vs/code/electron-sandbox/workbench/workbench.js' }, + { src: [...windowBootstrapFiles, 'out-build/vs/code/electron-sandbox/issue/issueReporter.js'], out: 'vs/code/electron-sandbox/issue/issueReporter.js' }, + { src: [...windowBootstrapFiles, 'out-build/vs/code/electron-sandbox/processExplorer/processExplorer.js'], out: 'vs/code/electron-sandbox/processExplorer/processExplorer.js' }, + { src: [...windowBootstrapFiles, 'out-build/vs/code/electron-browser/sharedProcess/sharedProcess.js'], out: 'vs/code/electron-browser/sharedProcess/sharedProcess.js' } + ] + } + ) )); gulp.task(optimizeVSCodeTask); @@ -105,7 +135,7 @@ const sourceMappingURLBase = `https://ticino.blob.core.windows.net/sourcemaps/${ const minifyVSCodeTask = task.define('minify-vscode', task.series( optimizeVSCodeTask, util.rimraf('out-vscode-min'), - common.minifyTask('out-vscode', `${sourceMappingURLBase}/core`) + optimize.minifyTask('out-vscode', `${sourceMappingURLBase}/core`) )); gulp.task(minifyVSCodeTask); @@ -211,7 +241,7 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op .pipe(json(packageJsonUpdates)); const date = new Date().toISOString(); - const productJsonUpdate = { commit, date, checksums }; + const productJsonUpdate = { commit, date, checksums, version }; if (shouldSetupSettingsSearch()) { productJsonUpdate.settingsSearchBuildId = getSettingsSearchBuildId(packageJson); diff --git a/build/gulpfile.vscode.web.js b/build/gulpfile.vscode.web.js index 8e92d7717ab..c5b09393728 100644 --- a/build/gulpfile.vscode.web.js +++ b/build/gulpfile.vscode.web.js @@ -10,7 +10,7 @@ const path = require('path'); const es = require('event-stream'); const util = require('./lib/util'); const task = require('./lib/task'); -const common = require('./lib/optimize'); +const optimize = require('./lib/optimize'); const product = require('../product.json'); const rename = require('gulp-rename'); const filter = require('gulp-filter'); @@ -153,24 +153,28 @@ exports.createVSCodeWebFileContentMapper = createVSCodeWebFileContentMapper; const optimizeVSCodeWebTask = task.define('optimize-vscode-web', task.series( util.rimraf('out-vscode-web'), - common.optimizeTask({ - src: 'out-build', - entryPoints: _.flatten(vscodeWebEntryPoints), - otherSources: [], - resources: vscodeWebResources, - loaderConfig: common.loaderConfig(), - externalLoaderInfo: util.createExternalLoaderConfig(product.webEndpointUrl, commit, quality), - out: 'out-vscode-web', - inlineAmdImages: true, - bundleInfo: undefined, - fileContentMapper: createVSCodeWebFileContentMapper('.build/web/extensions', product) - }) + optimize.optimizeTask( + { + out: 'out-vscode-web', + amd: { + src: 'out-build', + entryPoints: _.flatten(vscodeWebEntryPoints), + otherSources: [], + resources: vscodeWebResources, + loaderConfig: optimize.loaderConfig(), + externalLoaderInfo: util.createExternalLoaderConfig(product.webEndpointUrl, commit, quality), + inlineAmdImages: true, + bundleInfo: undefined, + fileContentMapper: createVSCodeWebFileContentMapper('.build/web/extensions', product) + } + } + ) )); const minifyVSCodeWebTask = task.define('minify-vscode-web', task.series( optimizeVSCodeWebTask, util.rimraf('out-vscode-web-min'), - common.minifyTask('out-vscode-web', `https://ticino.blob.core.windows.net/sourcemaps/${commit}/core`) + optimize.minifyTask('out-vscode-web', `https://ticino.blob.core.windows.net/sourcemaps/${commit}/core`) )); gulp.task(minifyVSCodeWebTask); diff --git a/build/lib/optimize.js b/build/lib/optimize.js index 8333afc7184..92e449a0cea 100644 --- a/build/lib/optimize.js +++ b/build/lib/optimize.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.minifyTask = exports.optimizeTask = exports.loaderConfig = void 0; +exports.minifyTask = exports.optimizeTask = exports.optimizeLoaderTask = exports.loaderConfig = void 0; const es = require("event-stream"); const gulp = require("gulp"); const concat = require("gulp-concat"); @@ -135,56 +135,94 @@ const DEFAULT_FILE_HEADER = [ ' * Copyright (C) Microsoft Corporation. All rights reserved.', ' *--------------------------------------------------------*/' ].join('\n'); -function optimizeTask(opts) { +function optimizeAMDTask(opts) { const src = opts.src; const entryPoints = opts.entryPoints; const resources = opts.resources; const loaderConfig = opts.loaderConfig; const bundledFileHeader = opts.header || DEFAULT_FILE_HEADER; - const bundleLoader = (typeof opts.bundleLoader === 'undefined' ? true : opts.bundleLoader); - const out = opts.out; const fileContentMapper = opts.fileContentMapper || ((contents, _path) => contents); - return function () { - const sourcemaps = require('gulp-sourcemaps'); - const bundlesStream = es.through(); // this stream will contain the bundled files - const resourcesStream = es.through(); // this stream will contain the resources - const bundleInfoStream = es.through(); // this stream will contain bundleInfo.json - bundle.bundle(entryPoints, loaderConfig, function (err, result) { - if (err || !result) { - return bundlesStream.emit('error', JSON.stringify(err)); + const sourcemaps = require('gulp-sourcemaps'); + const bundlesStream = es.through(); // this stream will contain the bundled files + const resourcesStream = es.through(); // this stream will contain the resources + const bundleInfoStream = es.through(); // this stream will contain bundleInfo.json + bundle.bundle(entryPoints, loaderConfig, function (err, result) { + if (err || !result) { + return bundlesStream.emit('error', JSON.stringify(err)); + } + toBundleStream(src, bundledFileHeader, result.files, fileContentMapper).pipe(bundlesStream); + // Remove css inlined resources + const filteredResources = resources.slice(); + result.cssInlinedResources.forEach(function (resource) { + if (process.env['VSCODE_BUILD_VERBOSE']) { + log('optimizer', 'excluding inlined: ' + resource); } - toBundleStream(src, bundledFileHeader, result.files, fileContentMapper).pipe(bundlesStream); - // Remove css inlined resources - const filteredResources = resources.slice(); - result.cssInlinedResources.forEach(function (resource) { - if (process.env['VSCODE_BUILD_VERBOSE']) { - log('optimizer', 'excluding inlined: ' + resource); - } - filteredResources.push('!' + resource); - }); - gulp.src(filteredResources, { base: `${src}`, allowEmpty: true }).pipe(resourcesStream); - const bundleInfoArray = []; - if (opts.bundleInfo) { - bundleInfoArray.push(new VinylFile({ - path: 'bundleInfo.json', - base: '.', - contents: Buffer.from(JSON.stringify(result.bundleData, null, '\t')) - })); - } - es.readArray(bundleInfoArray).pipe(bundleInfoStream); + filteredResources.push('!' + resource); }); - const result = es.merge(loader(src, bundledFileHeader, bundleLoader, opts.externalLoaderInfo), bundlesStream, resourcesStream, bundleInfoStream); - return result - .pipe(sourcemaps.write('./', { - sourceRoot: undefined, - addComment: true, - includeContent: true - })) - .pipe(opts.languages && opts.languages.length ? (0, i18n_1.processNlsFiles)({ - fileHeader: bundledFileHeader, - languages: opts.languages - }) : es.through()) - .pipe(gulp.dest(out)); + gulp.src(filteredResources, { base: `${src}`, allowEmpty: true }).pipe(resourcesStream); + const bundleInfoArray = []; + if (opts.bundleInfo) { + bundleInfoArray.push(new VinylFile({ + path: 'bundleInfo.json', + base: '.', + contents: Buffer.from(JSON.stringify(result.bundleData, null, '\t')) + })); + } + es.readArray(bundleInfoArray).pipe(bundleInfoStream); + }); + const result = es.merge(loader(src, bundledFileHeader, false, opts.externalLoaderInfo), bundlesStream, resourcesStream, bundleInfoStream); + return result + .pipe(sourcemaps.write('./', { + sourceRoot: undefined, + addComment: true, + includeContent: true + })) + .pipe(opts.languages && opts.languages.length ? (0, i18n_1.processNlsFiles)({ + fileHeader: bundledFileHeader, + languages: opts.languages + }) : es.through()); +} +function optimizeCommonJSTask(opts) { + const esbuild = require('esbuild'); + const src = opts.src; + const entryPoints = opts.entryPoints; + return gulp.src(entryPoints, { base: `${src}`, allowEmpty: true }) + .pipe(es.map((f, cb) => { + esbuild.build({ + entryPoints: [f.path], + bundle: true, + platform: opts.platform, + write: false, + external: opts.external + }).then(res => { + const jsFile = res.outputFiles[0]; + f.contents = Buffer.from(jsFile.contents); + cb(undefined, f); + }); + })); +} +function optimizeManualTask(options) { + const concatenations = options.map(opt => { + return gulp + .src(opt.src) + .pipe(concat(opt.out)); + }); + return es.merge(...concatenations); +} +function optimizeLoaderTask(src, out, bundleLoader, bundledFileHeader = '', externalLoaderInfo) { + return () => loader(src, bundledFileHeader, bundleLoader, externalLoaderInfo).pipe(gulp.dest(out)); +} +exports.optimizeLoaderTask = optimizeLoaderTask; +function optimizeTask(opts) { + return function () { + const optimizers = [optimizeAMDTask(opts.amd)]; + if (opts.commonJS) { + optimizers.push(optimizeCommonJSTask(opts.commonJS)); + } + if (opts.manual) { + optimizers.push(optimizeManualTask(opts.manual)); + } + return es.merge(...optimizers).pipe(gulp.dest(opts.out)); }; } exports.optimizeTask = optimizeTask; diff --git a/build/lib/optimize.ts b/build/lib/optimize.ts index c7b1981a589..2e3943d79f9 100644 --- a/build/lib/optimize.ts +++ b/build/lib/optimize.ts @@ -153,7 +153,7 @@ function toBundleStream(src: string, bundledFileHeader: string, bundles: bundle. })); } -export interface IOptimizeTaskOpts { +export interface IOptimizeAMDTaskOpts { /** * The folder to read files from. */ @@ -184,11 +184,7 @@ export interface IOptimizeTaskOpts { */ bundleInfo: boolean; /** - * (out folder name) - */ - out: string; - /** - * (out folder name) + * Language configuration. */ languages?: Language[]; /** @@ -205,67 +201,164 @@ const DEFAULT_FILE_HEADER = [ ' *--------------------------------------------------------*/' ].join('\n'); -export function optimizeTask(opts: IOptimizeTaskOpts): () => NodeJS.ReadWriteStream { +function optimizeAMDTask(opts: IOptimizeAMDTaskOpts): NodeJS.ReadWriteStream { const src = opts.src; const entryPoints = opts.entryPoints; const resources = opts.resources; const loaderConfig = opts.loaderConfig; const bundledFileHeader = opts.header || DEFAULT_FILE_HEADER; - const bundleLoader = (typeof opts.bundleLoader === 'undefined' ? true : opts.bundleLoader); - const out = opts.out; const fileContentMapper = opts.fileContentMapper || ((contents: string, _path: string) => contents); - return function () { - const sourcemaps = require('gulp-sourcemaps') as typeof import('gulp-sourcemaps'); + const sourcemaps = require('gulp-sourcemaps') as typeof import('gulp-sourcemaps'); - const bundlesStream = es.through(); // this stream will contain the bundled files - const resourcesStream = es.through(); // this stream will contain the resources - const bundleInfoStream = es.through(); // this stream will contain bundleInfo.json + const bundlesStream = es.through(); // this stream will contain the bundled files + const resourcesStream = es.through(); // this stream will contain the resources + const bundleInfoStream = es.through(); // this stream will contain bundleInfo.json - bundle.bundle(entryPoints, loaderConfig, function (err, result) { - if (err || !result) { return bundlesStream.emit('error', JSON.stringify(err)); } + bundle.bundle(entryPoints, loaderConfig, function (err, result) { + if (err || !result) { return bundlesStream.emit('error', JSON.stringify(err)); } - toBundleStream(src, bundledFileHeader, result.files, fileContentMapper).pipe(bundlesStream); + toBundleStream(src, bundledFileHeader, result.files, fileContentMapper).pipe(bundlesStream); - // Remove css inlined resources - const filteredResources = resources.slice(); - result.cssInlinedResources.forEach(function (resource) { - if (process.env['VSCODE_BUILD_VERBOSE']) { - log('optimizer', 'excluding inlined: ' + resource); - } - filteredResources.push('!' + resource); - }); - gulp.src(filteredResources, { base: `${src}`, allowEmpty: true }).pipe(resourcesStream); - - const bundleInfoArray: VinylFile[] = []; - if (opts.bundleInfo) { - bundleInfoArray.push(new VinylFile({ - path: 'bundleInfo.json', - base: '.', - contents: Buffer.from(JSON.stringify(result.bundleData, null, '\t')) - })); + // Remove css inlined resources + const filteredResources = resources.slice(); + result.cssInlinedResources.forEach(function (resource) { + if (process.env['VSCODE_BUILD_VERBOSE']) { + log('optimizer', 'excluding inlined: ' + resource); } - es.readArray(bundleInfoArray).pipe(bundleInfoStream); + filteredResources.push('!' + resource); }); + gulp.src(filteredResources, { base: `${src}`, allowEmpty: true }).pipe(resourcesStream); - const result = es.merge( - loader(src, bundledFileHeader, bundleLoader, opts.externalLoaderInfo), - bundlesStream, - resourcesStream, - bundleInfoStream - ); + const bundleInfoArray: VinylFile[] = []; + if (opts.bundleInfo) { + bundleInfoArray.push(new VinylFile({ + path: 'bundleInfo.json', + base: '.', + contents: Buffer.from(JSON.stringify(result.bundleData, null, '\t')) + })); + } + es.readArray(bundleInfoArray).pipe(bundleInfoStream); + }); - return result - .pipe(sourcemaps.write('./', { - sourceRoot: undefined, - addComment: true, - includeContent: true - })) - .pipe(opts.languages && opts.languages.length ? processNlsFiles({ - fileHeader: bundledFileHeader, - languages: opts.languages - }) : es.through()) - .pipe(gulp.dest(out)); + const result = es.merge( + loader(src, bundledFileHeader, false, opts.externalLoaderInfo), + bundlesStream, + resourcesStream, + bundleInfoStream + ); + + return result + .pipe(sourcemaps.write('./', { + sourceRoot: undefined, + addComment: true, + includeContent: true + })) + .pipe(opts.languages && opts.languages.length ? processNlsFiles({ + fileHeader: bundledFileHeader, + languages: opts.languages + }) : es.through()); +} + +export interface IOptimizeCommonJSTaskOpts { + /** + * The paths to consider for optimizing. + */ + entryPoints: string[]; + /** + * The folder to read files from. + */ + src: string; + /** + * ESBuild `platform` option: https://esbuild.github.io/api/#platform + */ + platform: 'browser' | 'node' | 'neutral'; + /** + * ESBuild `external` option: https://esbuild.github.io/api/#external + */ + external: string[]; +} + +function optimizeCommonJSTask(opts: IOptimizeCommonJSTaskOpts): NodeJS.ReadWriteStream { + const esbuild = require('esbuild') as typeof import('esbuild'); + + const src = opts.src; + const entryPoints = opts.entryPoints; + + return gulp.src(entryPoints, { base: `${src}`, allowEmpty: true }) + .pipe(es.map((f: any, cb) => { + esbuild.build({ + entryPoints: [f.path], + bundle: true, + platform: opts.platform, + write: false, + external: opts.external + }).then(res => { + const jsFile = res.outputFiles[0]; + f.contents = Buffer.from(jsFile.contents); + + cb(undefined, f); + }); + })); +} + +export interface IOptimizeManualTaskOpts { + /** + * The paths to consider for concatenation. The entries + * will be concatenated in the order they are provided. + */ + src: string[]; + /** + * Destination target to concatenate the entryPoints into. + */ + out: string; +} + +function optimizeManualTask(options: IOptimizeManualTaskOpts[]): NodeJS.ReadWriteStream { + const concatenations = options.map(opt => { + return gulp + .src(opt.src) + .pipe(concat(opt.out)); + }); + + return es.merge(...concatenations); +} + +export function optimizeLoaderTask(src: string, out: string, bundleLoader: boolean, bundledFileHeader = '', externalLoaderInfo?: any): () => NodeJS.ReadWriteStream { + return () => loader(src, bundledFileHeader, bundleLoader, externalLoaderInfo).pipe(gulp.dest(out)); +} + +export interface IOptimizeTaskOpts { + /** + * Destination folder for the optimized files. + */ + out: string; + /** + * Optimize AMD modules (using our AMD loader). + */ + amd: IOptimizeAMDTaskOpts; + /** + * Optimize CommonJS modules (using esbuild). + */ + commonJS?: IOptimizeCommonJSTaskOpts; + /** + * Optimize manually by concatenating files. + */ + manual?: IOptimizeManualTaskOpts[]; +} + +export function optimizeTask(opts: IOptimizeTaskOpts): () => NodeJS.ReadWriteStream { + return function () { + const optimizers = [optimizeAMDTask(opts.amd)]; + if (opts.commonJS) { + optimizers.push(optimizeCommonJSTask(opts.commonJS)); + } + + if (opts.manual) { + optimizers.push(optimizeManualTask(opts.manual)); + } + + return es.merge(...optimizers).pipe(gulp.dest(opts.out)); }; } diff --git a/build/lib/util.ts b/build/lib/util.ts index 038fb096d92..6fb799f2699 100644 --- a/build/lib/util.ts +++ b/build/lib/util.ts @@ -451,4 +451,3 @@ export function buildWebNodePaths(outDir: string) { result.taskName = 'build-web-node-paths'; return result; } - diff --git a/cli/src/constants.rs b/cli/src/constants.rs index 533657abefc..a194f9ab9a1 100644 --- a/cli/src/constants.rs +++ b/cli/src/constants.rs @@ -12,6 +12,8 @@ pub const VSCODE_CLI_VERSION: Option<&'static str> = option_env!("VSCODE_CLI_VER pub const VSCODE_CLI_ASSET_NAME: Option<&'static str> = option_env!("VSCODE_CLI_ASSET_NAME"); pub const VSCODE_CLI_AI_KEY: Option<&'static str> = option_env!("VSCODE_CLI_AI_KEY"); pub const VSCODE_CLI_AI_ENDPOINT: Option<&'static str> = option_env!("VSCODE_CLI_AI_ENDPOINT"); +pub const VSCODE_CLI_UPDATE_ENDPOINT: Option<&'static str> = + option_env!("VSCODE_CLI_UPDATE_ENDPOINT"); pub const TUNNEL_SERVICE_USER_AGENT_ENV_VAR: &str = "TUNNEL_SERVICE_USER_AGENT"; diff --git a/cli/src/update_service.rs b/cli/src/update_service.rs index c37715c8a0b..e2513f6ab80 100644 --- a/cli/src/update_service.rs +++ b/cli/src/update_service.rs @@ -8,9 +8,12 @@ use std::path::Path; use serde::Deserialize; use crate::{ + constants::VSCODE_CLI_UPDATE_ENDPOINT, debug, log, options, spanf, util::{ - errors::{AnyError, StatusError, UnsupportedPlatformError, WrappedError}, + errors::{ + AnyError, StatusError, UnsupportedPlatformError, UpdatesNotConfigured, WrappedError, + }, io::ReportCopyProgress, }, }; @@ -54,11 +57,13 @@ impl UpdateService { quality: options::Quality, version: &str, ) -> Result { + let update_endpoint = VSCODE_CLI_UPDATE_ENDPOINT.ok_or(UpdatesNotConfigured())?; let download_segment = target .download_segment(platform) .ok_or(UnsupportedPlatformError())?; let download_url = format!( - "https://update.code.visualstudio.com/api/versions/{}/{}/{}", + "{}/api/versions/{}/{}/{}", + update_endpoint, version, download_segment, quality_download_segment(quality), @@ -92,11 +97,13 @@ impl UpdateService { target: TargetKind, quality: options::Quality, ) -> Result { + let update_endpoint = VSCODE_CLI_UPDATE_ENDPOINT.ok_or(UpdatesNotConfigured())?; let download_segment = target .download_segment(platform) .ok_or(UnsupportedPlatformError())?; let download_url = format!( - "https://update.code.visualstudio.com/api/latest/{}/{}", + "{}/api/latest/{}/{}", + update_endpoint, download_segment, quality_download_segment(quality), ); @@ -127,13 +134,15 @@ impl UpdateService { &self, release: &Release, ) -> Result { + let update_endpoint = VSCODE_CLI_UPDATE_ENDPOINT.ok_or(UpdatesNotConfigured())?; let download_segment = release .target .download_segment(release.platform) .ok_or(UnsupportedPlatformError())?; let download_url = format!( - "https://update.code.visualstudio.com/commit:{}/{}/{}", + "{}/commit:{}/{}/{}", + update_endpoint, release.commit, download_segment, quality_download_segment(release.quality), diff --git a/cli/src/util/errors.rs b/cli/src/util/errors.rs index 9c643446b6e..47f6a64e359 100644 --- a/cli/src/util/errors.rs +++ b/cli/src/util/errors.rs @@ -316,6 +316,14 @@ impl std::fmt::Display for ServerHasClosed { } } +#[derive(Debug)] +pub struct UpdatesNotConfigured(); + +impl std::fmt::Display for UpdatesNotConfigured { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "Update service is not configured") + } +} #[derive(Debug)] pub struct ServiceAlreadyRegistered(); @@ -408,7 +416,8 @@ makeAnyError!( CannotForwardControlPort, ServerHasClosed, ServiceAlreadyRegistered, - WindowsNeedsElevation + WindowsNeedsElevation, + UpdatesNotConfigured ); impl From for AnyError { diff --git a/extensions/cpp/language-configuration.json b/extensions/cpp/language-configuration.json index 2bf73923d91..9a88814ff35 100644 --- a/extensions/cpp/language-configuration.json +++ b/extensions/cpp/language-configuration.json @@ -6,8 +6,7 @@ "brackets": [ ["{", "}"], ["[", "]"], - ["(", ")"], - ["#if","#endif"], + ["(", ")"] ], "autoClosingPairs": [ { "open": "[", "close": "]" }, diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 1419f3d8cac..1b002799be9 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -1148,21 +1148,38 @@ export class CommandCenter { } @command('git.acceptMerge') - async acceptMerge(uri: Uri | unknown): Promise { - if (!(uri instanceof Uri)) { + async acceptMerge(_uri: Uri | unknown): Promise { + const { activeTab } = window.tabGroups.activeTabGroup; + if (!activeTab) { return; } + + if (!(activeTab.input instanceof TabInputTextMerge)) { + return; + } + + const uri = activeTab.input.result; + const repository = this.model.getRepository(uri); if (!repository) { console.log(`FAILED to accept merge because uri ${uri.toString()} doesn't belong to any repository`); return; } - const { activeTab } = window.tabGroups.activeTabGroup; - if (!activeTab) { + const result = await commands.executeCommand('mergeEditor.acceptMerge') as { successful: boolean }; + if (result.successful) { + await repository.add([uri]); + await commands.executeCommand('workbench.view.scm'); + } + + /* + if (!(uri instanceof Uri)) { return; } + + + // make sure to save the merged document const doc = workspace.textDocuments.find(doc => doc.uri.toString() === uri.toString()); if (!doc) { @@ -1185,7 +1202,7 @@ export class CommandCenter { if (didCloseTab) { await repository.add([uri]); await commands.executeCommand('workbench.view.scm'); - } + }*/ } @command('git.runGitMerge') diff --git a/extensions/git/src/statusbar.ts b/extensions/git/src/statusbar.ts index bfb61d4266c..e24306ff800 100644 --- a/extensions/git/src/statusbar.ts +++ b/extensions/git/src/statusbar.ts @@ -25,12 +25,12 @@ class CheckoutStatusBar { get command(): Command | undefined { const rebasing = !!this.repository.rebaseCommit; const isBranchProtected = this.repository.isBranchProtected(); - const title = `${isBranchProtected ? '$(lock)' : '$(git-branch)'} ${this.repository.headLabel}${rebasing ? ` (${localize('rebasing', 'Rebasing')})` : ''}`; + const label = `${this.repository.headLabel}${rebasing ? ` (${localize('rebasing', 'Rebasing')})` : ''}`; return { command: 'git.checkout', - tooltip: localize('checkout', "Checkout branch/tag..."), - title, + tooltip: localize('checkout', "{0}, Checkout branch/tag...", label), + title: `${isBranchProtected ? '$(lock)' : '$(git-branch)'} ${label}`, arguments: [this.repository.sourceControl] }; } diff --git a/extensions/github-authentication/src/experimentationService.ts b/extensions/github-authentication/src/experimentationService.ts index e3297ddd50e..16923309b4e 100644 --- a/extensions/github-authentication/src/experimentationService.ts +++ b/extensions/github-authentication/src/experimentationService.ts @@ -18,14 +18,19 @@ export class ExperimentationTelemetry implements IExperimentationTelemetry { switch (vscode.env.uriScheme) { case 'vscode': targetPopulation = TargetPopulation.Public; + break; case 'vscode-insiders': targetPopulation = TargetPopulation.Insiders; + break; case 'vscode-exploration': targetPopulation = TargetPopulation.Internal; + break; case 'code-oss': targetPopulation = TargetPopulation.Team; + break; default: targetPopulation = TargetPopulation.Public; + break; } const id = this.context.extension.id; diff --git a/extensions/github/package.json b/extensions/github/package.json index dd59ef3046d..1b21d4b52a1 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -77,13 +77,15 @@ "file/share": [ { "command": "github.copyVscodeDevLinkFile", - "when": "github.hasGitHubRepo" + "when": "github.hasGitHubRepo", + "group": "0_vscode@0" } ], "editor/context/share": [ { "command": "github.copyVscodeDevLink", - "when": "github.hasGitHubRepo && resourceScheme != untitled" + "when": "github.hasGitHubRepo && resourceScheme != untitled", + "group": "0_vscode@0" } ] }, diff --git a/extensions/image-preview/src/imagePreview/index.ts b/extensions/image-preview/src/imagePreview/index.ts index 37dc18c4c0f..5c0d56933ba 100644 --- a/extensions/image-preview/src/imagePreview/index.ts +++ b/extensions/image-preview/src/imagePreview/index.ts @@ -119,6 +119,12 @@ class ImagePreview extends MediaPreview { this.webviewEditor.webview.postMessage({ type: 'setActive', value: this.webviewEditor.active }); } + public override dispose(): void { + super.dispose(); + this.sizeStatusBarEntry.hide(this); + this.zoomStatusBarEntry.hide(this); + } + public zoomIn() { if (this.previewState === PreviewState.Active) { this.webviewEditor.webview.postMessage({ type: 'zoomIn' }); diff --git a/extensions/image-preview/src/mediaPreview.ts b/extensions/image-preview/src/mediaPreview.ts index 7c56c3fe6ae..424f197cc34 100644 --- a/extensions/image-preview/src/mediaPreview.ts +++ b/extensions/image-preview/src/mediaPreview.ts @@ -48,10 +48,8 @@ export abstract class MediaPreview extends Disposable { })); this._register(webviewEditor.onDidDispose(() => { - if (this.previewState === PreviewState.Active) { - this.binarySizeStatusBarEntry.hide(this); - } this.previewState = PreviewState.Disposed; + this.dispose(); })); const watcher = this._register(vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(resource, '*'))); @@ -69,6 +67,11 @@ export abstract class MediaPreview extends Disposable { })); } + public override dispose() { + super.dispose(); + this.binarySizeStatusBarEntry.hide(this); + } + protected updateBinarySize() { vscode.workspace.fs.stat(this.resource).then(({ size }) => { this._binarySize = size; diff --git a/extensions/markdown-language-features/notebook/index.ts b/extensions/markdown-language-features/notebook/index.ts index 05ebb019dec..5728903e212 100644 --- a/extensions/markdown-language-features/notebook/index.ts +++ b/extensions/markdown-language-features/notebook/index.ts @@ -8,21 +8,27 @@ import MarkdownIt from 'markdown-it'; import type * as MarkdownItToken from 'markdown-it/lib/token'; import type { ActivationFunction } from 'vscode-notebook-renderer'; -const allowedHtmlTags = Object.freeze([ - 'a', +const allowedHtmlTags = Object.freeze(['a', + 'abbr', 'b', + 'bdo', 'blockquote', 'br', - 'button', 'caption', - 'center', + 'cite', 'code', 'col', 'colgroup', + 'dd', + 'del', 'details', + 'dfn', 'div', + 'dl', + 'dt', 'em', - 'font', + 'figcaption', + 'figure', 'h1', 'h2', 'h3', @@ -32,16 +38,23 @@ const allowedHtmlTags = Object.freeze([ 'hr', 'i', 'img', - 'input', + 'ins', 'kbd', 'label', 'li', + 'mark', 'ol', 'p', 'pre', - 'select', + 'q', + 'rp', + 'rt', + 'ruby', + 'samp', + 'small', 'small', 'span', + 'strike', 'strong', 'sub', 'summary', @@ -49,15 +62,17 @@ const allowedHtmlTags = Object.freeze([ 'table', 'tbody', 'td', - 'textarea', 'tfoot', 'th', 'thead', + 'time', 'tr', 'tt', 'u', 'ul', + 'var', 'video', + 'wbr', ]); const allowedSvgTags = Object.freeze([ @@ -377,7 +392,7 @@ function slugify(text: string): string { .toLowerCase() .replace(/\s+/g, '-') // Replace whitespace with - // allow-any-unicode-next-line - .replace(/[\]\[\!\'\#\$\%\&\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~\`。,、;:?!…—·ˉ¨‘’“”々~‖∶"'`|〃〔〕〈〉《》「」『』.〖〗【】()[]{}]/g, '') // Remove known punctuators + .replace(/[\]\[\!\/\'\"\#\$\%\&\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\{\|\}\~\`。,、;:?!…—·ˉ¨‘’“”々~‖∶"'`|〃〔〕〈〉《》「」『』.〖〗【】()[]{}]/g, '') // Remove known punctuators .replace(/^\-+/, '') // Remove leading - .replace(/\-+$/, '') // Remove trailing - ); diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index 43e55685b12..49c00bf6154 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -434,81 +434,63 @@ "experimental" ] }, - "markdown.experimental.validate.enabled": { + "markdown.validate.enabled": { "type": "boolean", "scope": "resource", - "description": "%configuration.markdown.experimental.validate.enabled.description%", - "default": false, - "tags": [ - "experimental" - ] + "description": "%configuration.markdown.validate.enabled.description%", + "default": false }, - "markdown.experimental.validate.referenceLinks.enabled": { + "markdown.validate.referenceLinks.enabled": { "type": "string", "scope": "resource", - "markdownDescription": "%configuration.markdown.experimental.validate.referenceLinks.enabled.description%", + "markdownDescription": "%configuration.markdown.validate.referenceLinks.enabled.description%", "default": "warning", "enum": [ "ignore", "warning", "error" - ], - "tags": [ - "experimental" ] }, - "markdown.experimental.validate.fragmentLinks.enabled": { + "markdown.validate.fragmentLinks.enabled": { "type": "string", "scope": "resource", - "markdownDescription": "%configuration.markdown.experimental.validate.fragmentLinks.enabled.description%", + "markdownDescription": "%configuration.markdown.validate.fragmentLinks.enabled.description%", "default": "warning", "enum": [ "ignore", "warning", "error" - ], - "tags": [ - "experimental" ] }, - "markdown.experimental.validate.fileLinks.enabled": { + "markdown.validate.fileLinks.enabled": { "type": "string", "scope": "resource", - "markdownDescription": "%configuration.markdown.experimental.validate.fileLinks.enabled.description%", + "markdownDescription": "%configuration.markdown.validate.fileLinks.enabled.description%", "default": "warning", "enum": [ "ignore", "warning", "error" - ], - "tags": [ - "experimental" ] }, - "markdown.experimental.validate.fileLinks.markdownFragmentLinks": { + "markdown.validate.fileLinks.markdownFragmentLinks": { "type": "string", "scope": "resource", - "markdownDescription": "%configuration.markdown.experimental.validate.fileLinks.markdownFragmentLinks.description%", + "markdownDescription": "%configuration.markdown.validate.fileLinks.markdownFragmentLinks.description%", "default": "ignore", "enum": [ "ignore", "warning", "error" - ], - "tags": [ - "experimental" ] }, - "markdown.experimental.validate.ignoreLinks": { + "markdown.validate.ignoredLinks": { "type": "array", "scope": "resource", - "markdownDescription": "%configuration.markdown.experimental.validate.ignoreLinks.description%", + "markdownDescription": "%configuration.markdown.validate.ignoredLinks.description%", "items": { "type": "string" - }, - "tags": [ - "experimental" - ] + } }, "markdown.experimental.updateLinksOnFileMove.enabled": { "type": "string", diff --git a/extensions/markdown-language-features/package.nls.json b/extensions/markdown-language-features/package.nls.json index d4cea703cc2..3aae783308d 100644 --- a/extensions/markdown-language-features/package.nls.json +++ b/extensions/markdown-language-features/package.nls.json @@ -31,12 +31,12 @@ "configuration.markdown.suggest.paths.enabled.description": "Enable/disable path suggestions for markdown links", "configuration.markdown.editor.drop.enabled": "Enable/disable dropping into the markdown editor to insert shift. Requires enabling `#editor.dropIntoEditor.enabled#`.", "configuration.markdown.editor.pasteLinks.enabled": "Enable/disable pasting files into a Markdown editor inserts Markdown links. Requires enabling `#editor.experimental.pasteActions.enabled#`.", - "configuration.markdown.experimental.validate.enabled.description": "Enable/disable all error reporting in Markdown files.", - "configuration.markdown.experimental.validate.referenceLinks.enabled.description": "Validate reference links in Markdown files, e.g. `[link][ref]`. Requires enabling `#markdown.experimental.validate.enabled#`.", - "configuration.markdown.experimental.validate.fragmentLinks.enabled.description": "Validate fragment links to headers in the current Markdown file, e.g. `[link](#header)`. Requires enabling `#markdown.experimental.validate.enabled#`.", - "configuration.markdown.experimental.validate.fileLinks.enabled.description": "Validate links to other files in Markdown files, e.g. `[link](/path/to/file.md)`. This checks that the target files exists. Requires enabling `#markdown.experimental.validate.enabled#`.", - "configuration.markdown.experimental.validate.fileLinks.markdownFragmentLinks.description": "Validate the fragment part of links to headers in other files in Markdown files, e.g. `[link](/path/to/file.md#header)`. Inherits the setting value from `#markdown.experimental.validate.fragmentLinks.enabled#` by default.", - "configuration.markdown.experimental.validate.ignoreLinks.description": "Configure links that should not be validated. For example `/about` would not validate the link `[about](/about)`, while the glob `/assets/**/*.svg` would let you skip validation for any link to `.svg` files under the `assets` directory.", + "configuration.markdown.validate.enabled.description": "Enable/disable all error reporting in Markdown files.", + "configuration.markdown.validate.referenceLinks.enabled.description": "Validate reference links in Markdown files, e.g. `[link][ref]`. Requires enabling `#markdown.validate.enabled#`.", + "configuration.markdown.validate.fragmentLinks.enabled.description": "Validate fragment links to headers in the current Markdown file, e.g. `[link](#header)`. Requires enabling `#markdown.validate.enabled#`.", + "configuration.markdown.validate.fileLinks.enabled.description": "Validate links to other files in Markdown files, e.g. `[link](/path/to/file.md)`. This checks that the target files exists. Requires enabling `#markdown.validate.enabled#`.", + "configuration.markdown.validate.fileLinks.markdownFragmentLinks.description": "Validate the fragment part of links to headers in other files in Markdown files, e.g. `[link](/path/to/file.md#header)`. Inherits the setting value from `#markdown.validate.fragmentLinks.enabled#` by default.", + "configuration.markdown.validate.ignoredLinks.description": "Configure links that should not be validated. For example adding `/about` would not validate the link `[about](/about)`, while the glob `/assets/**/*.svg` would let you skip validation for any link to `.svg` files under the `assets` directory.", "configuration.markdown.experimental.updateLinksOnFileMove.enabled": "Try to update links in Markdown files when a file is renamed/moved in the workspace. Use `#markdown.experimental.updateLinksOnFileMove.externalFileGlobs#` to configure which files trigger link updates.", "configuration.markdown.experimental.updateLinksOnFileMove.enabled.prompt": "Prompt on each file move.", "configuration.markdown.experimental.updateLinksOnFileMove.enabled.always": "Always update links automatically.", diff --git a/extensions/markdown-language-features/server/README.md b/extensions/markdown-language-features/server/README.md index 49670fc800d..761baf37719 100644 --- a/extensions/markdown-language-features/server/README.md +++ b/extensions/markdown-language-features/server/README.md @@ -36,7 +36,7 @@ This server uses the [Markdown Language Service](https://github.com/microsoft/vs - (experimental) Updating links when a file is moved / renamed. Uses a custom `markdown/getEditForFileRenames` message. -- (experimental) [Pull diagnostics (validation)](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_pullDiagnostics) for links. +- [Pull diagnostics (validation)](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_pullDiagnostics) for links. ## Client requirements @@ -56,17 +56,16 @@ The server supports the following settings: - `suggest` - `paths` - `enabled` — Enable/disable path suggestions. - - `experimental` - - `validate` - - `enabled` — Enable/disable all validation. - - `referenceLinks` - - `enabled` — Enable/disable validation of reference links: `[text][ref]` - - `fragmentLinks` - - `enabled` — Enable/disable validation of links to fragments in the current files: `[text](#head)` - - `fileLinks` - - `enabled` — Enable/disable validation of links to file in the workspace. - - `markdownFragmentLinks` — Enable/disable validation of links to headers in other Markdown files. - - `ignoreLinks` — Array of glob patterns for files that should not be validated. + - `validate` + - `enabled` — Enable/disable all validation. + - `referenceLinks` + - `enabled` — Enable/disable validation of reference links: `[text][ref]` + - `fragmentLinks` + - `enabled` — Enable/disable validation of links to fragments in the current files: `[text](#head)` + - `fileLinks` + - `enabled` — Enable/disable validation of links to file in the workspace. + - `markdownFragmentLinks` — Enable/disable validation of links to headers in other Markdown files. + - `ignoredLinks` — Array of glob patterns for files that should not be validated. ### Custom requests diff --git a/extensions/markdown-language-features/server/package.json b/extensions/markdown-language-features/server/package.json index 2f153ddae9a..c38c618f05d 100644 --- a/extensions/markdown-language-features/server/package.json +++ b/extensions/markdown-language-features/server/package.json @@ -13,7 +13,7 @@ "vscode-languageserver": "^8.0.2", "vscode-languageserver-textdocument": "^1.0.5", "vscode-languageserver-types": "^3.17.1", - "vscode-markdown-languageservice": "^0.1.0-alpha.6", + "vscode-markdown-languageservice": "^0.1.0-alpha.10", "vscode-nls": "^5.0.1", "vscode-uri": "^3.0.3" }, diff --git a/extensions/markdown-language-features/server/src/config.ts b/extensions/markdown-language-features/server/src/config.ts index 6f9d85fbf1d..5992258b0b6 100644 --- a/extensions/markdown-language-features/server/src/config.ts +++ b/extensions/markdown-language-features/server/src/config.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { LsConfiguration } from 'vscode-markdown-languageservice/out/config'; +import { LsConfiguration } from 'vscode-markdown-languageservice'; export { LsConfiguration }; diff --git a/extensions/markdown-language-features/server/src/configuration.ts b/extensions/markdown-language-features/server/src/configuration.ts index 5066b3110a6..69358261f82 100644 --- a/extensions/markdown-language-features/server/src/configuration.ts +++ b/extensions/markdown-language-features/server/src/configuration.ts @@ -16,21 +16,19 @@ interface Settings { }; }; - readonly experimental: { - readonly validate: { - readonly enabled: true; - readonly referenceLinks: { - readonly enabled: ValidateEnabled; - }; - readonly fragmentLinks: { - readonly enabled: ValidateEnabled; - }; - readonly fileLinks: { - readonly enabled: ValidateEnabled; - readonly markdownFragmentLinks: ValidateEnabled; - }; - readonly ignoreLinks: readonly string[]; + readonly validate: { + readonly enabled: true; + readonly referenceLinks: { + readonly enabled: ValidateEnabled; }; + readonly fragmentLinks: { + readonly enabled: ValidateEnabled; + }; + readonly fileLinks: { + readonly enabled: ValidateEnabled; + readonly markdownFragmentLinks: ValidateEnabled; + }; + readonly ignoredLinks: readonly string[]; }; }; } @@ -56,4 +54,4 @@ export class ConfigurationManager extends Disposable { public getSettings(): Settings | undefined { return this._settings; } -} \ No newline at end of file +} diff --git a/extensions/markdown-language-features/server/src/languageFeatures/diagnostics.ts b/extensions/markdown-language-features/server/src/languageFeatures/diagnostics.ts index 092f337f949..d0f4d9c73af 100644 --- a/extensions/markdown-language-features/server/src/languageFeatures/diagnostics.ts +++ b/extensions/markdown-language-features/server/src/languageFeatures/diagnostics.ts @@ -5,10 +5,10 @@ import { Connection, FullDocumentDiagnosticReport, TextDocuments, UnchangedDocumentDiagnosticReport } from 'vscode-languageserver'; import * as md from 'vscode-markdown-languageservice'; -import { disposeAll } from 'vscode-markdown-languageservice/out/util/dispose'; import { Disposable } from 'vscode-notebook-renderer/events'; import { URI } from 'vscode-uri'; import { ConfigurationManager, ValidateEnabled } from '../configuration'; +import { disposeAll } from '../util/dispose'; const defaultDiagnosticOptions: md.DiagnosticOptions = { validateFileLinks: md.DiagnosticLevel.ignore, @@ -34,11 +34,11 @@ function getDiagnosticsOptions(config: ConfigurationManager): md.DiagnosticOptio } return { - validateFileLinks: convertDiagnosticLevel(settings.markdown.experimental.validate.fileLinks.enabled), - validateReferences: convertDiagnosticLevel(settings.markdown.experimental.validate.referenceLinks.enabled), - validateFragmentLinks: convertDiagnosticLevel(settings.markdown.experimental.validate.fragmentLinks.enabled), - validateMarkdownFileLinkFragments: convertDiagnosticLevel(settings.markdown.experimental.validate.fileLinks.markdownFragmentLinks), - ignoreLinks: settings.markdown.experimental.validate.ignoreLinks, + validateFileLinks: convertDiagnosticLevel(settings.markdown.validate.fileLinks.enabled), + validateReferences: convertDiagnosticLevel(settings.markdown.validate.referenceLinks.enabled), + validateFragmentLinks: convertDiagnosticLevel(settings.markdown.validate.fragmentLinks.enabled), + validateMarkdownFileLinkFragments: convertDiagnosticLevel(settings.markdown.validate.fileLinks.markdownFragmentLinks), + ignoreLinks: settings.markdown.validate.ignoredLinks, }; } @@ -69,7 +69,7 @@ export function registerValidateSupport( connection.languages.diagnostics.on(async (params, token): Promise => { logger.log(md.LogLevel.Trace, 'Server: connection.languages.diagnostics.on', params.textDocument.uri); - if (!config.getSettings()?.markdown.experimental.validate.enabled) { + if (!config.getSettings()?.markdown.validate.enabled) { return emptyDiagnosticsResponse; } diff --git a/extensions/markdown-language-features/server/src/protocol.ts b/extensions/markdown-language-features/server/src/protocol.ts index ab407b4afc5..e1dc9aee785 100644 --- a/extensions/markdown-language-features/server/src/protocol.ts +++ b/extensions/markdown-language-features/server/src/protocol.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { RequestType } from 'vscode-languageserver'; +import { FileRename, RequestType } from 'vscode-languageserver'; import type * as lsp from 'vscode-languageserver-types'; import type * as md from 'vscode-markdown-languageservice'; @@ -22,7 +22,7 @@ export const findMarkdownFilesInWorkspace = new RequestType<{}, string[], any>(' //#region To server export const getReferencesToFileInWorkspace = new RequestType<{ uri: string }, lsp.Location[], any>('markdown/getReferencesToFileInWorkspace'); -export const getEditForFileRenames = new RequestType, lsp.WorkspaceEdit, any>('markdown/getEditForFileRenames'); +export const getEditForFileRenames = new RequestType('markdown/getEditForFileRenames'); export const fs_watcher_onChange = new RequestType<{ id: number; uri: string; kind: 'create' | 'change' | 'delete' }, void, any>('markdown/fs/watcher/onChange'); diff --git a/extensions/markdown-language-features/server/src/server.ts b/extensions/markdown-language-features/server/src/server.ts index a45607af422..95a088b439b 100644 --- a/extensions/markdown-language-features/server/src/server.ts +++ b/extensions/markdown-language-features/server/src/server.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken, Connection, InitializeParams, InitializeResult, NotebookDocuments, TextDocuments } from 'vscode-languageserver'; +import { CancellationToken, Connection, InitializeParams, InitializeResult, NotebookDocuments, ResponseError, TextDocuments } from 'vscode-languageserver'; import { TextDocument } from 'vscode-languageserver-textdocument'; import * as lsp from 'vscode-languageserver-types'; import * as md from 'vscode-markdown-languageservice'; @@ -170,7 +170,16 @@ export async function startServer(connection: Connection, serverConfig: { if (!document) { return undefined; } - return mdLs!.prepareRename(document, params.position, token); + + try { + return await mdLs!.prepareRename(document, params.position, token); + } catch (e) { + if (e instanceof md.RenameNotSupportedAtLocationError) { + throw new ResponseError(0, e.message); + } else { + throw e; + } + } }); connection.onRenameRequest(async (params, token) => { @@ -228,7 +237,15 @@ export async function startServer(connection: Connection, serverConfig: { })); connection.onRequest(protocol.getEditForFileRenames, (async (params, token: CancellationToken) => { - return mdLs!.getRenameFilesInWorkspaceEdit(params.map(x => ({ oldUri: URI.parse(x.oldUri), newUri: URI.parse(x.newUri) })), token); + const result = await mdLs!.getRenameFilesInWorkspaceEdit(params.map(x => ({ oldUri: URI.parse(x.oldUri), newUri: URI.parse(x.newUri) })), token); + if (!result) { + return result; + } + + return { + edit: result.edit, + participatingRenames: result.participatingRenames.map(rename => ({ oldUri: rename.oldUri.toString(), newUri: rename.newUri.toString() })) + }; })); connection.onRequest(protocol.resolveLinkTarget, (async (params, token: CancellationToken) => { diff --git a/extensions/markdown-language-features/server/yarn.lock b/extensions/markdown-language-features/server/yarn.lock index 54f91c0a3bd..00347dd2bd8 100644 --- a/extensions/markdown-language-features/server/yarn.lock +++ b/extensions/markdown-language-features/server/yarn.lock @@ -42,10 +42,10 @@ vscode-languageserver@^8.0.2: dependencies: vscode-languageserver-protocol "3.17.2" -vscode-markdown-languageservice@^0.1.0-alpha.6: - version "0.1.0-alpha.6" - resolved "https://registry.yarnpkg.com/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.1.0-alpha.6.tgz#cc82c608d816b7b73459e648bf29ed49946d5425" - integrity sha512-EhWN8Y3HJTgI2XR39F0sHSRLMbTTKiMbQedf/d4SJupA1ks0kPQyQz/Ystp1Aua44Z/nNcYo4UNNFOOM0sxRPw== +vscode-markdown-languageservice@^0.1.0-alpha.10: + version "0.1.0-alpha.10" + resolved "https://registry.yarnpkg.com/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.1.0-alpha.10.tgz#012dcf600b9d1a738cd7071f17627285342d17c7" + integrity sha512-GZTxGZp49BIf/k5plc5x+Bp70kmwaTdt523p+wifG9AQ0uKMSRcwmlKu8mOcJUd0ZvDR3ORI/Cze90Dy5HCM2A== dependencies: picomatch "^2.3.1" vscode-languageserver-textdocument "^1.0.5" diff --git a/extensions/markdown-language-features/src/client/protocol.ts b/extensions/markdown-language-features/src/client/protocol.ts index 92ec2c0e80a..f906460fce9 100644 --- a/extensions/markdown-language-features/src/client/protocol.ts +++ b/extensions/markdown-language-features/src/client/protocol.ts @@ -5,7 +5,7 @@ import type Token = require('markdown-it/lib/token'); import * as vscode from 'vscode'; -import { RequestType } from 'vscode-languageclient'; +import { FileRename, RequestType } from 'vscode-languageclient'; import type * as lsp from 'vscode-languageserver-types'; import type * as md from 'vscode-markdown-languageservice'; @@ -30,7 +30,7 @@ export const findMarkdownFilesInWorkspace = new RequestType<{}, string[], any>(' //#region To server export const getReferencesToFileInWorkspace = new RequestType<{ uri: string }, lsp.Location[], any>('markdown/getReferencesToFileInWorkspace'); -export const getEditForFileRenames = new RequestType, lsp.WorkspaceEdit, any>('markdown/getEditForFileRenames'); +export const getEditForFileRenames = new RequestType, { participatingRenames: readonly FileRename[]; edit: lsp.WorkspaceEdit }, any>('markdown/getEditForFileRenames'); export const fs_watcher_onChange = new RequestType<{ id: number; uri: string; kind: 'create' | 'change' | 'delete' }, void, any>('markdown/fs/watcher/onChange'); diff --git a/extensions/markdown-language-features/src/languageFeatures/copyPaste.ts b/extensions/markdown-language-features/src/languageFeatures/copyPaste.ts index 5f6eb16b972..960d01b7a19 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyPaste.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyPaste.ts @@ -85,7 +85,7 @@ class PasteEditProvider implements vscode.DocumentPasteEditProvider { const baseName = path.basename(file.name, ext); for (let i = 0; ; ++i) { const name = i === 0 ? baseName : `${baseName}-${i}`; - const uri = vscode.Uri.joinPath(root, `${name}.${ext}`); + const uri = vscode.Uri.joinPath(root, `${name}${ext}`); try { await vscode.workspace.fs.stat(uri); } catch { diff --git a/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts b/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts index 6ae36b84aaf..30c6b5be49c 100644 --- a/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts +++ b/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts @@ -33,7 +33,7 @@ class AddToIgnoreLinksQuickFixProvider implements vscode.CodeActionProvider { const commandReg = commandManager.register({ id: AddToIgnoreLinksQuickFixProvider._addToIgnoreLinksCommandId, execute(resource: vscode.Uri, path: string) { - const settingId = 'experimental.validate.ignoreLinks'; + const settingId = 'validate.ignoredLinks'; const config = vscode.workspace.getConfiguration('markdown', resource); const paths = new Set(config.get(settingId, [])); paths.add(path); diff --git a/extensions/markdown-language-features/src/languageFeatures/linkUpdater.ts b/extensions/markdown-language-features/src/languageFeatures/linkUpdater.ts index f0209a60edc..cfe16893b39 100644 --- a/extensions/markdown-language-features/src/languageFeatures/linkUpdater.ts +++ b/extensions/markdown-language-features/src/languageFeatures/linkUpdater.ts @@ -69,18 +69,11 @@ class UpdateLinksOnFileRenameHandler extends Disposable { const renames = Array.from(this._pendingRenames); this._pendingRenames.clear(); - const edit = new vscode.WorkspaceEdit(); - const resourcesBeingRenamed: vscode.Uri[] = []; + const result = await this.getEditsForFileRename(renames, noopToken); - for (const { oldUri, newUri } of renames) { - if (await this.withEditsForFileRename(edit, oldUri, newUri, noopToken)) { - resourcesBeingRenamed.push(newUri); - } - } - - if (edit.size) { - if (await this.confirmActionWithUser(resourcesBeingRenamed)) { - await vscode.workspace.applyEdit(edit); + if (result && result.edit.size) { + if (await this.confirmActionWithUser(result.resourcesBeingRenamed)) { + await vscode.workspace.applyEdit(result.edit); } } } @@ -194,25 +187,25 @@ class UpdateLinksOnFileRenameHandler extends Disposable { return false; } - private async withEditsForFileRename( - workspaceEdit: vscode.WorkspaceEdit, - oldUri: vscode.Uri, - newUri: vscode.Uri, - token: vscode.CancellationToken, - ): Promise { - const edit = await this.client.getEditForFileRenames([{ oldUri: oldUri.toString(), newUri: newUri.toString() }], token); - if (!edit.documentChanges?.length) { - return false; + private async getEditsForFileRename(renames: readonly RenameAction[], token: vscode.CancellationToken): Promise<{ edit: vscode.WorkspaceEdit; resourcesBeingRenamed: vscode.Uri[] } | undefined> { + const result = await this.client.getEditForFileRenames(renames.map(rename => ({ oldUri: rename.oldUri.toString(), newUri: rename.newUri.toString() })), token); + if (!result?.edit.documentChanges?.length) { + return undefined; } - for (const change of edit.documentChanges as TextDocumentEdit[]) { + const workspaceEdit = new vscode.WorkspaceEdit(); + + for (const change of result.edit.documentChanges as TextDocumentEdit[]) { const uri = vscode.Uri.parse(change.textDocument.uri); for (const edit of change.edits) { workspaceEdit.replace(uri, convertRange(edit.range), edit.newText); } } - return true; + return { + edit: workspaceEdit, + resourcesBeingRenamed: result.participatingRenames.map(x => vscode.Uri.parse(x.newUri)), + }; } private getConfirmMessage(start: string, resourcesToConfirm: readonly vscode.Uri[]): string { diff --git a/extensions/merge-conflict/package.json b/extensions/merge-conflict/package.json index d10085c4fc6..978341508f8 100644 --- a/extensions/merge-conflict/package.json +++ b/extensions/merge-conflict/package.json @@ -34,43 +34,50 @@ "category": "%command.category%", "title": "%command.accept.all-current%", "original": "Accept All Current", - "command": "merge-conflict.accept.all-current" + "command": "merge-conflict.accept.all-current", + "enablement": "!isMergeEditor" }, { "category": "%command.category%", "title": "%command.accept.all-incoming%", "original": "Accept All Incoming", - "command": "merge-conflict.accept.all-incoming" + "command": "merge-conflict.accept.all-incoming", + "enablement": "!isMergeEditor" }, { "category": "%command.category%", "title": "%command.accept.all-both%", "original": "Accept All Both", - "command": "merge-conflict.accept.all-both" + "command": "merge-conflict.accept.all-both", + "enablement": "!isMergeEditor" }, { "category": "%command.category%", "title": "%command.accept.current%", "original": "Accept Current", - "command": "merge-conflict.accept.current" + "command": "merge-conflict.accept.current", + "enablement": "!isMergeEditor" }, { "category": "%command.category%", "title": "%command.accept.incoming%", "original": "Accept Incoming", - "command": "merge-conflict.accept.incoming" + "command": "merge-conflict.accept.incoming", + "enablement": "!isMergeEditor" }, { "category": "%command.category%", "title": "%command.accept.selection%", "original": "Accept Selection", - "command": "merge-conflict.accept.selection" + "command": "merge-conflict.accept.selection", + "enablement": "!isMergeEditor" }, { "category": "%command.category%", "title": "%command.accept.both%", "original": "Accept Both", - "command": "merge-conflict.accept.both" + "command": "merge-conflict.accept.both", + "enablement": "!isMergeEditor" }, { "category": "%command.category%", @@ -92,7 +99,8 @@ "category": "%command.category%", "title": "%command.compare%", "original": "Compare Current Conflict", - "command": "merge-conflict.compare" + "command": "merge-conflict.compare", + "enablement": "!isMergeEditor" } ], "menus": { diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index c0ed6b05f13..64b10a008ae 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -39,7 +39,7 @@ "jsonc-parser": "^2.2.1", "semver": "5.5.1", "vscode-nls": "^5.1.0", - "vscode-tas-client": "^0.1.47", + "vscode-tas-client": "^0.1.63", "vscode-uri": "^3.0.3" }, "devDependencies": { diff --git a/extensions/typescript-language-features/src/experimentationService.ts b/extensions/typescript-language-features/src/experimentationService.ts index cb566874433..f739872a824 100644 --- a/extensions/typescript-language-features/src/experimentationService.ts +++ b/extensions/typescript-language-features/src/experimentationService.ts @@ -35,14 +35,19 @@ export class ExperimentationService implements vscode.Disposable { switch (vscode.env.uriScheme) { case 'vscode': targetPopulation = tas.TargetPopulation.Public; + break; case 'vscode-insiders': targetPopulation = tas.TargetPopulation.Insiders; + break; case 'vscode-exploration': targetPopulation = tas.TargetPopulation.Internal; + break; case 'code-oss': targetPopulation = tas.TargetPopulation.Team; + break; default: targetPopulation = tas.TargetPopulation.Public; + break; } const id = this._extensionContext.extension.id; diff --git a/extensions/typescript-language-features/yarn.lock b/extensions/typescript-language-features/yarn.lock index 81f56c011a9..08e22bf4c15 100644 --- a/extensions/typescript-language-features/yarn.lock +++ b/extensions/typescript-language-features/yarn.lock @@ -78,10 +78,10 @@ semver@5.5.1: resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477" integrity sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw== -tas-client@0.1.45: - version "0.1.45" - resolved "https://registry.yarnpkg.com/tas-client/-/tas-client-0.1.45.tgz#83bbf73f8458a0f527f9a389f7e1c37f63a64a76" - integrity sha512-IG9UmCpDbDPK23UByQ27rLybkRZYEx2eC9EkieXdwPKKjZPD2zPwfQmyGnZrZet4FUt3yj0ytkwz+liR9Nz/nA== +tas-client@0.1.58: + version "0.1.58" + resolved "https://registry.yarnpkg.com/tas-client/-/tas-client-0.1.58.tgz#67d66bf0e27df5276ebc751105e6ad47791c36d8" + integrity sha512-fOWii4wQXuo9Zl0oXgvjBzZWzKc5MmUR6XQWX93WU2c1SaP1plPo/zvXP8kpbZ9fvegFOHdapszYqMTRq/SRtg== dependencies: axios "^0.26.1" @@ -90,12 +90,12 @@ vscode-nls@^5.1.0: resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.1.0.tgz#443b301a7465d88c81c0f4e1914f9857f0dce1e4" integrity sha512-37Ha44QrLFwR2IfSSYdOArzUvOyoWbOYTwQC+wS0NfqKjhW7s0WQ1lMy5oJXgSZy9sAiZS5ifELhbpXodeMR8w== -vscode-tas-client@^0.1.47: - version "0.1.47" - resolved "https://registry.yarnpkg.com/vscode-tas-client/-/vscode-tas-client-0.1.47.tgz#d66795cbbaa231aba659b6c40d43927d73596375" - integrity sha512-SlEPDi+0gwxor4ANzBtXwqROPQdQkClHeVJgnkvdDF5Xnl407htCsabTPAq4Di8muObORtLchqQS/k1ocaGDEg== +vscode-tas-client@^0.1.63: + version "0.1.63" + resolved "https://registry.yarnpkg.com/vscode-tas-client/-/vscode-tas-client-0.1.63.tgz#df89e67e9bf7ecb46471a0fb8a4a522d2aafad65" + integrity sha512-TY5TPyibzi6rNmuUB7eRVqpzLzNfQYrrIl/0/F8ukrrbzOrKVvS31hM3urE+tbaVrnT+TMYXL16GhX57vEowhA== dependencies: - tas-client "0.1.45" + tas-client "0.1.58" vscode-uri@^3.0.3: version "3.0.3" diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.api.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.api.test.ts index e447db1326c..51b82032c77 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.api.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.api.test.ts @@ -251,44 +251,6 @@ const apiTestContentProvider: vscode.NotebookContentProvider = { }); }); -(vscode.env.uiKind === vscode.UIKind.Web ? suite.skip : suite)('statusbar', () => { - const emitter = new vscode.EventEmitter(); - const onDidCallProvide = emitter.event; - const suiteDisposables: vscode.Disposable[] = []; - suiteTeardown(async function () { - assertNoRpc(); - - await revertAllDirty(); - await closeAllEditors(); - - disposeAll(suiteDisposables); - suiteDisposables.length = 0; - }); - - suiteSetup(() => { - suiteDisposables.push(vscode.notebooks.registerNotebookCellStatusBarItemProvider('notebookCoreTest', { - async provideCellStatusBarItems(cell: vscode.NotebookCell, _token: vscode.CancellationToken): Promise { - emitter.fire(cell); - return []; - } - })); - - suiteDisposables.push(vscode.workspace.registerNotebookContentProvider('notebookCoreTest', apiTestContentProvider)); - }); - - test('provideCellStatusBarItems called on metadata change', async function () { - const provideCalled = asPromise(onDidCallProvide); - const notebook = await openRandomNotebookDocument(); - await vscode.window.showNotebookDocument(notebook); - await provideCalled; - - const edit = new vscode.WorkspaceEdit(); - edit.set(notebook.uri, [vscode.NotebookEdit.updateCellMetadata(0, { inputCollapsed: true })]); - await vscode.workspace.applyEdit(edit); - await provideCalled; - }); -}); - suite('Notebook & LiveShare', function () { const suiteDisposables: vscode.Disposable[] = []; diff --git a/package.json b/package.json index 7a71f462808..9957d764a7f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.72.0", - "distro": "97c54ace90a9ab3cea766c76da997942f2ae4e8d", + "distro": "d9fc5ec0abd44d6d2eab4ad5c0cf4ca9c4e839e1", "author": { "name": "Microsoft Corporation" }, @@ -86,19 +86,19 @@ "vscode-proxy-agent": "^0.12.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "7.0.1", - "xterm": "5.0.0", - "xterm-addon-canvas": "0.2.0", - "xterm-addon-search": "0.10.0", - "xterm-addon-serialize": "0.8.0", - "xterm-addon-unicode11": "0.4.0", - "xterm-addon-webgl": "0.13.0", - "xterm-headless": "5.0.0", + "xterm": "5.1.0-beta.1", + "xterm-addon-canvas": "0.3.0-beta.1", + "xterm-addon-search": "0.11.0-beta.1", + "xterm-addon-serialize": "0.9.0-beta.1", + "xterm-addon-unicode11": "0.5.0-beta.1", + "xterm-addon-webgl": "0.14.0-beta.2", + "xterm-headless": "5.1.0-beta.1", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, "devDependencies": { "7zip": "0.0.6", - "@playwright/test": "1.24.2", + "@playwright/test": "1.26.0", "@swc/cli": "0.1.57", "@swc/core": "1.2.245", "@types/cookie": "^0.3.3", @@ -206,7 +206,7 @@ "ts-loader": "^9.2.7", "ts-node": "^10.9.1", "tsec": "0.1.4", - "typescript": "^4.9.0-dev.20220916", + "typescript": "^4.9.0-dev.20220921", "typescript-formatter": "7.1.0", "underscore": "^1.12.1", "util": "^0.12.4", diff --git a/remote/package.json b/remote/package.json index 6a15f54d756..c6d2d2547ad 100644 --- a/remote/package.json +++ b/remote/package.json @@ -24,13 +24,13 @@ "vscode-proxy-agent": "^0.12.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "7.0.1", - "xterm": "5.0.0", - "xterm-addon-canvas": "0.2.0", - "xterm-addon-search": "0.10.0", - "xterm-addon-serialize": "0.8.0", - "xterm-addon-unicode11": "0.4.0", - "xterm-addon-webgl": "0.13.0", - "xterm-headless": "5.0.0", + "xterm": "5.1.0-beta.1", + "xterm-addon-canvas": "0.3.0-beta.1", + "xterm-addon-search": "0.11.0-beta.1", + "xterm-addon-serialize": "0.9.0-beta.1", + "xterm-addon-unicode11": "0.5.0-beta.1", + "xterm-addon-webgl": "0.14.0-beta.2", + "xterm-headless": "5.1.0-beta.1", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, diff --git a/remote/web/package.json b/remote/web/package.json index 0d38acec84a..d7e6c49c66f 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -11,10 +11,10 @@ "tas-client-umd": "0.1.6", "vscode-oniguruma": "1.6.1", "vscode-textmate": "7.0.1", - "xterm": "5.0.0", - "xterm-addon-canvas": "0.2.0", - "xterm-addon-search": "0.10.0", - "xterm-addon-unicode11": "0.4.0", - "xterm-addon-webgl": "0.13.0" + "xterm": "5.1.0-beta.1", + "xterm-addon-canvas": "0.3.0-beta.1", + "xterm-addon-search": "0.11.0-beta.1", + "xterm-addon-unicode11": "0.5.0-beta.1", + "xterm-addon-webgl": "0.14.0-beta.2" } } diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index 6252456eb40..c9a0cd1bacf 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -68,27 +68,27 @@ vscode-textmate@7.0.1: resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-7.0.1.tgz#8118a32b02735dccd14f893b495fa5389ad7de79" integrity sha512-zQ5U/nuXAAMsh691FtV0wPz89nSkHbs+IQV8FDk+wew9BlSDhf4UmWGlWJfTR2Ti6xZv87Tj5fENzKf6Qk7aLw== -xterm-addon-canvas@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/xterm-addon-canvas/-/xterm-addon-canvas-0.2.0.tgz#ba0080d4071f172f94e8c0b5e6151dd7e386f1a1" - integrity sha512-b4tMT05US9Rlqv6R0XZTHsfq8MRKzwxITwpvckuod/l4lokcCokHPbgpYAytOgrzqkzWjYI+Ol8en6cMGf8ncg== +xterm-addon-canvas@0.3.0-beta.1: + version "0.3.0-beta.1" + resolved "https://registry.yarnpkg.com/xterm-addon-canvas/-/xterm-addon-canvas-0.3.0-beta.1.tgz#17a65f5da65416b01d620ddef6247ff5013ffc15" + integrity sha512-34PKhrkvK1RtlOOmni4i5GUIyoFKGMph8fWFvA2d52IDTKmX9YoLzZfU73D/sUAx+/GKobCE8sr14CuBZctgNw== -xterm-addon-search@0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.10.0.tgz#b6a5e859c0bfd83ad534233f93376640c0e0c652" - integrity sha512-l+kjDxNDQbkniU5OUo9BHknxUEPZGM0OFpVpc2sMmrb97S0FKJVJO4wAZPJvSGVJ8ZEG6KuDyzXluvnb08t71Q== +xterm-addon-search@0.11.0-beta.1: + version "0.11.0-beta.1" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.11.0-beta.1.tgz#fe7178d70246cde73550447c5524672575467499" + integrity sha512-fKj8KnnhH1nC4oZpKsgnhtgxkTctoa9kGLMpTJjsNzFu0VvXvLGIRezTPI75UEIQdEdaxcwB7/aKelQTO+72LA== -xterm-addon-unicode11@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.4.0.tgz#59a4abbb591befb69ca0c5f7c3f9fa9c1c05353e" - integrity sha512-HkUwR4gc8MKVFy2Ux8zUnjqARYpfl7dJ9na3TwRbAUbF4JlCv707m4Z07WVaDMIRUZsfZ+5LgSi+Ss7PfZqNcw== +xterm-addon-unicode11@0.5.0-beta.1: + version "0.5.0-beta.1" + resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.5.0-beta.1.tgz#8a9e9356018e082318abbe2be1f9599fcc6b46a2" + integrity sha512-uAErX4gwhW6N524stLG6oZR3yBGgPnFmZ2Tv4vyYy7tcgDuHRoc22xYSCDgO1ohz1FLlOm8JGXRjXliwO9ic3A== -xterm-addon-webgl@0.13.0: - version "0.13.0" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.13.0.tgz#b1d42ec454390ad8595aa8c8dde714b98a5eb896" - integrity sha512-xL4qBQWUHjFR620/8VHCtrTMVQsnZaAtd1IxFoiKPhC63wKp6b+73a45s97lb34yeo57PoqZhE9Jq5pB++ksPQ== +xterm-addon-webgl@0.14.0-beta.2: + version "0.14.0-beta.2" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.14.0-beta.2.tgz#832c31b52b78fb67a65bbd23c9fb850caceb43ae" + integrity sha512-1ccbkJiUZ5ojnoAEJsbdV0jMZaYSnZ02wfV8yBU243u6TTgvCzZ7nq5BR9bT+5K/ESFWiekobfybxHwuYnylmQ== -xterm@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.0.0.tgz#0af50509b33d0dc62fde7a4ec17750b8e453cc5c" - integrity sha512-tmVsKzZovAYNDIaUinfz+VDclraQpPUnAME+JawosgWRMphInDded/PuY0xmU5dOhyeYZsI0nz5yd8dPYsdLTA== +xterm@5.1.0-beta.1: + version "5.1.0-beta.1" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.1.0-beta.1.tgz#a6617c6887066d166632d1e69b6eb83a179d8b63" + integrity sha512-ml7bqjO23bh4yu7qXKogXtCy4SbDTV21rfDXUvLPPaxrlQus6NoN1byy1eFH4ONWpv5ZHGeItRdQ/X00et9Pcw== diff --git a/remote/yarn.lock b/remote/yarn.lock index 5fc10a11df4..7b384ed44b9 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -788,40 +788,40 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -xterm-addon-canvas@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/xterm-addon-canvas/-/xterm-addon-canvas-0.2.0.tgz#ba0080d4071f172f94e8c0b5e6151dd7e386f1a1" - integrity sha512-b4tMT05US9Rlqv6R0XZTHsfq8MRKzwxITwpvckuod/l4lokcCokHPbgpYAytOgrzqkzWjYI+Ol8en6cMGf8ncg== +xterm-addon-canvas@0.3.0-beta.1: + version "0.3.0-beta.1" + resolved "https://registry.yarnpkg.com/xterm-addon-canvas/-/xterm-addon-canvas-0.3.0-beta.1.tgz#17a65f5da65416b01d620ddef6247ff5013ffc15" + integrity sha512-34PKhrkvK1RtlOOmni4i5GUIyoFKGMph8fWFvA2d52IDTKmX9YoLzZfU73D/sUAx+/GKobCE8sr14CuBZctgNw== -xterm-addon-search@0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.10.0.tgz#b6a5e859c0bfd83ad534233f93376640c0e0c652" - integrity sha512-l+kjDxNDQbkniU5OUo9BHknxUEPZGM0OFpVpc2sMmrb97S0FKJVJO4wAZPJvSGVJ8ZEG6KuDyzXluvnb08t71Q== +xterm-addon-search@0.11.0-beta.1: + version "0.11.0-beta.1" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.11.0-beta.1.tgz#fe7178d70246cde73550447c5524672575467499" + integrity sha512-fKj8KnnhH1nC4oZpKsgnhtgxkTctoa9kGLMpTJjsNzFu0VvXvLGIRezTPI75UEIQdEdaxcwB7/aKelQTO+72LA== -xterm-addon-serialize@0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.8.0.tgz#715b510b91cc8e0d32844ca2a7de4d0fb5d49d9d" - integrity sha512-8N4RBaxM4TwlZRFXYz+xJwHUeFRXscRIM9O3wq26T0DrG7lM9JprSq1F4IGO1I5Et/CqXjiBFD50KwqkRKF0/w== +xterm-addon-serialize@0.9.0-beta.1: + version "0.9.0-beta.1" + resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.9.0-beta.1.tgz#44a8047ec85abe4db232acc58c53355dd314bf6d" + integrity sha512-jVkpU5GC728ko0k190o+M1xubMkhRolKj18160rxlZhd0Sm/1yHUtFneC9pYSsLypynd3Te5LnZnHfEgVmka4g== -xterm-addon-unicode11@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.4.0.tgz#59a4abbb591befb69ca0c5f7c3f9fa9c1c05353e" - integrity sha512-HkUwR4gc8MKVFy2Ux8zUnjqARYpfl7dJ9na3TwRbAUbF4JlCv707m4Z07WVaDMIRUZsfZ+5LgSi+Ss7PfZqNcw== +xterm-addon-unicode11@0.5.0-beta.1: + version "0.5.0-beta.1" + resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.5.0-beta.1.tgz#8a9e9356018e082318abbe2be1f9599fcc6b46a2" + integrity sha512-uAErX4gwhW6N524stLG6oZR3yBGgPnFmZ2Tv4vyYy7tcgDuHRoc22xYSCDgO1ohz1FLlOm8JGXRjXliwO9ic3A== -xterm-addon-webgl@0.13.0: - version "0.13.0" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.13.0.tgz#b1d42ec454390ad8595aa8c8dde714b98a5eb896" - integrity sha512-xL4qBQWUHjFR620/8VHCtrTMVQsnZaAtd1IxFoiKPhC63wKp6b+73a45s97lb34yeo57PoqZhE9Jq5pB++ksPQ== +xterm-addon-webgl@0.14.0-beta.2: + version "0.14.0-beta.2" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.14.0-beta.2.tgz#832c31b52b78fb67a65bbd23c9fb850caceb43ae" + integrity sha512-1ccbkJiUZ5ojnoAEJsbdV0jMZaYSnZ02wfV8yBU243u6TTgvCzZ7nq5BR9bT+5K/ESFWiekobfybxHwuYnylmQ== -xterm-headless@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.0.0.tgz#6426e85aec24f1bdfd322fe4e5159e0113f059d4" - integrity sha512-d+3C883Tz5zNCcapJSUpZVO8lplKhjJZtvuL4oIMTE74BX/3UsK7zUQgp5c6KS5Vv4FjFSb3XQA+n2Cx9Znq+w== +xterm-headless@5.1.0-beta.1: + version "5.1.0-beta.1" + resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.1.0-beta.1.tgz#badec2e97e47aa44267a4de2c1b42b4d23ad49a2" + integrity sha512-V3G7l4pN6/HW//vKXryOCdDXVKdrQTQmtHEqkZ8waD68cJdeMdIoGYJuzavd5rHpxCqm/KR5O8ztI41jridong== -xterm@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.0.0.tgz#0af50509b33d0dc62fde7a4ec17750b8e453cc5c" - integrity sha512-tmVsKzZovAYNDIaUinfz+VDclraQpPUnAME+JawosgWRMphInDded/PuY0xmU5dOhyeYZsI0nz5yd8dPYsdLTA== +xterm@5.1.0-beta.1: + version "5.1.0-beta.1" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.1.0-beta.1.tgz#a6617c6887066d166632d1e69b6eb83a179d8b63" + integrity sha512-ml7bqjO23bh4yu7qXKogXtCy4SbDTV21rfDXUvLPPaxrlQus6NoN1byy1eFH4ONWpv5ZHGeItRdQ/X00et9Pcw== yallist@^4.0.0: version "4.0.0" diff --git a/src/bootstrap-amd.js b/src/bootstrap-amd.js index 31e27ea7ed7..88747c29aaf 100644 --- a/src/bootstrap-amd.js +++ b/src/bootstrap-amd.js @@ -6,6 +6,11 @@ //@ts-check 'use strict'; +// Store the node.js require function in a variable +// before loading our AMD loader to avoid issues +// when this file is bundled with other files. +const nodeRequire = require; + const loader = require('./vs/loader'); const bootstrap = require('./bootstrap'); const performance = require('./vs/base/common/performance'); @@ -17,7 +22,7 @@ const nlsConfig = bootstrap.setupNLS(); loader.config({ baseUrl: bootstrap.fileUriFromPath(__dirname, { isWindows: process.platform === 'win32' }), catchError: true, - nodeRequire: require, + nodeRequire, 'vs/nls': nlsConfig, amdModulesPattern: /^vs\//, recordStats: true diff --git a/src/buildfile.js b/src/buildfile.js index b63d85617c3..24654c77abe 100644 --- a/src/buildfile.js +++ b/src/buildfile.js @@ -35,7 +35,6 @@ exports.base = [ exclude: ['vs/nls'], prepend: [ { path: 'vs/loader.js' }, - { path: 'vs/nls.js', amdModuleId: 'vs/nls' }, { path: 'vs/base/worker/workerMain.js' } ], dest: 'vs/base/worker/workerMain.js' diff --git a/src/main.js b/src/main.js index 884120e01b0..f7e6e49a967 100644 --- a/src/main.js +++ b/src/main.js @@ -34,7 +34,7 @@ bootstrap.enableASARSupport(); // Set userData path before app 'ready' event const args = parseCLIArgs(); -const userDataPath = getUserDataPath(args); +const userDataPath = getUserDataPath(args, product.nameShort ?? 'code-oss-dev'); app.setPath('userData', userDataPath); // Resolve code cache path diff --git a/src/server-main.js b/src/server-main.js index 4e806323689..27ecbaed0d6 100644 --- a/src/server-main.js +++ b/src/server-main.js @@ -188,14 +188,10 @@ function sanitizeStringArg(val) { * @throws */ async function parsePort(host, strPort) { - let specificPort; if (strPort) { let range; if (strPort.match(/^\d+$/)) { - specificPort = parseInt(strPort, 10); - if (specificPort === 0) { - return specificPort; - } + return parseInt(strPort, 10); } else if (range = parseRange(strPort)) { const port = await findFreePort(host, range.start, range.end); if (port !== undefined) { diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 51566489870..beb9436ac70 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -1363,6 +1363,77 @@ const defaultSafeProtocols = [ Schemas.command, ]; +/** + * List of safe, non-input html tags. + */ +export const basicMarkupHtmlTags = Object.freeze([ + 'a', + 'abbr', + 'b', + 'bdo', + 'blockquote', + 'br', + 'caption', + 'cite', + 'code', + 'col', + 'colgroup', + 'dd', + 'del', + 'details', + 'dfn', + 'div', + 'dl', + 'dt', + 'em', + 'figcaption', + 'figure', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'hr', + 'i', + 'img', + 'ins', + 'kbd', + 'label', + 'li', + 'mark', + 'ol', + 'p', + 'pre', + 'q', + 'rp', + 'rt', + 'ruby', + 'samp', + 'small', + 'small', + 'span', + 'strike', + 'strong', + 'sub', + 'summary', + 'sup', + 'table', + 'tbody', + 'td', + 'tfoot', + 'th', + 'thead', + 'time', + 'tr', + 'tt', + 'u', + 'ul', + 'var', + 'video', + 'wbr', +]); + /** * Sanitizes the given `value` and reset the given `node` with it. */ diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index 063d60b9792..ccc1d8d7752 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -358,6 +358,26 @@ function sanitizeRenderedMarkdown( } } +export const allowedMarkdownAttr = [ + 'align', + 'alt', + 'class', + 'controls', + 'data-code', + 'data-href', + 'height', + 'href', + 'loop', + 'muted', + 'playsinline', + 'poster', + 'src', + 'style', + 'target', + 'title', + 'width', +]; + function getSanitizerOptions(options: { readonly isTrusted?: boolean }): { config: dompurify.Config; allowedSchemes: string[] } { const allowedSchemes = [ Schemas.http, @@ -380,8 +400,8 @@ function getSanitizerOptions(options: { readonly isTrusted?: boolean }): { confi // Since we have our own sanitize function for marked, it's possible we missed some tag so let dompurify make sure. // HTML tags that can result from markdown are from reading https://spec.commonmark.org/0.29/ // HTML table tags that can result from markdown are from https://github.github.com/gfm/#tables-extension- - ALLOWED_TAGS: ['ul', 'li', 'p', 'b', 'i', 'code', 'blockquote', 'ol', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'em', 'pre', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'div', 'del', 'a', 'strong', 'br', 'img', 'span'], - ALLOWED_ATTR: ['href', 'data-href', 'target', 'title', 'src', 'alt', 'class', 'style', 'data-code', 'width', 'height', 'align'], + ALLOWED_TAGS: [...DOM.basicMarkupHtmlTags], + ALLOWED_ATTR: allowedMarkdownAttr, ALLOW_UNKNOWN_PROTOCOLS: true, }, allowedSchemes diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index 7e9f2aecba6..577ea7879dc 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -22,6 +22,7 @@ import { IListDragAndDrop, IListDragEvent, IListGestureEvent, IListMouseEvent, I import { RangeMap, shift } from 'vs/base/browser/ui/list/rangeMap'; import { IRow, RowCache } from 'vs/base/browser/ui/list/rowCache'; import { IObservableValue } from 'vs/base/common/observableValue'; +import { BugIndicatingError } from 'vs/base/common/errors'; interface IItem { readonly id: string; @@ -1356,26 +1357,26 @@ export class ListView implements ISpliceable, IDisposable { const size = item.size; - if (!this.setRowHeight && item.row) { - const newSize = item.row.domNode.offsetHeight; - item.size = newSize; + if (item.row) { + item.row.domNode.style.height = ''; + item.size = item.row.domNode.offsetHeight; item.lastDynamicHeightWidth = this.renderWidth; - return newSize - size; + return item.size - size; } const row = this.cache.alloc(item.templateId); - row.domNode.style.height = ''; this.rowsContainer.appendChild(row.domNode); const renderer = this.renderers.get(item.templateId); - if (renderer) { - renderer.renderElement(item.element, index, row.templateData, undefined); - renderer.disposeElement?.(item.element, index, row.templateData, undefined); + if (!renderer) { + throw new BugIndicatingError('Missing renderer for templateId: ' + item.templateId); } + renderer.renderElement(item.element, index, row.templateData, undefined); item.size = row.domNode.offsetHeight; + renderer.disposeElement?.(item.element, index, row.templateData, undefined); this.virtualDelegate.setDynamicHeight?.(item.element, item.size); diff --git a/src/vs/base/browser/ui/table/table.css b/src/vs/base/browser/ui/table/table.css index 89edc6f0ffe..087f9d3ebf9 100644 --- a/src/vs/base/browser/ui/table/table.css +++ b/src/vs/base/browser/ui/table/table.css @@ -10,6 +10,7 @@ height: 100%; width: 100%; white-space: nowrap; + overflow: hidden; } .monaco-table > .monaco-split-view2 { diff --git a/src/vs/base/common/actions.ts b/src/vs/base/common/actions.ts index 30e5c974f3d..13db4add519 100644 --- a/src/vs/base/common/actions.ts +++ b/src/vs/base/common/actions.ts @@ -193,7 +193,7 @@ export class ActionRunner extends Disposable implements IActionRunner { } } -export class Separator extends Action { +export class Separator implements IAction { /** * Joins all non-empty lists of actions with separators. @@ -215,12 +215,14 @@ export class Separator extends Action { static readonly ID = 'vs.actions.separator'; - constructor(label?: string) { - super(Separator.ID, label, label ? 'separator text' : 'separator'); + readonly id: string = Separator.ID; - this.checked = false; - this.enabled = false; - } + readonly label: string = ''; + readonly tooltip: string = ''; + readonly class: string = ''; + readonly enabled: boolean = false; + readonly checked: boolean = false; + async run() { } } export class SubmenuAction implements IAction { diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index e8d7a30dfc4..89abcbaaf20 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -938,9 +938,11 @@ export class PauseableEmitter extends Emitter { if (this._mergeFn) { // use the merge function to create a single composite // event. make a copy in case firing pauses this emitter - const events = Array.from(this._eventQueue); - this._eventQueue.clear(); - super.fire(this._mergeFn(events)); + if (this._eventQueue.size > 0) { + const events = Array.from(this._eventQueue); + this._eventQueue.clear(); + super.fire(this._mergeFn(events)); + } } else { // no merging, fire each event individually and test diff --git a/src/vs/base/common/observableImpl/utils.ts b/src/vs/base/common/observableImpl/utils.ts index 2c11ff5eb5a..f78d35a9683 100644 --- a/src/vs/base/common/observableImpl/utils.ts +++ b/src/vs/base/common/observableImpl/utils.ts @@ -51,13 +51,23 @@ export function waitForState(observable: IObservable, pr export function waitForState(observable: IObservable, predicate: (state: T) => boolean): Promise; export function waitForState(observable: IObservable, predicate: (state: T) => boolean): Promise { return new Promise(resolve => { + let didRun = false; + let shouldDispose = false; const d = autorun('waitForState', reader => { const currentState = observable.read(reader); if (predicate(currentState)) { - d.dispose(); + if (!didRun) { + shouldDispose = true; + } else { + d.dispose(); + } resolve(currentState); } }); + didRun = true; + if (shouldDispose) { + d.dispose(); + } }); } diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 1620aa3bd2c..e07695c9363 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -70,6 +70,7 @@ export interface IProductConfiguration { readonly extensionsGallery?: { readonly serviceUrl: string; + readonly searchUrl?: string; readonly itemUrl: string; readonly publisherUrl: string; readonly resourceUrlTemplate: string; diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts index 97bd9b3b055..f66058f8b1f 100644 --- a/src/vs/base/common/strings.ts +++ b/src/vs/base/common/strings.ts @@ -600,6 +600,16 @@ export function getCharContainingOffset(str: string, offset: number): [number, n return [startOffset, endOffset]; } +export function charCount(str: string): number { + const iterator = new GraphemeIterator(str); + let length = 0; + while (!iterator.eol()) { + length++; + iterator.nextGraphemeLength(); + } + return length; +} + let CONTAINS_RTL: RegExp | undefined = undefined; function makeContainsRtl() { diff --git a/src/vs/base/test/common/event.test.ts b/src/vs/base/test/common/event.test.ts index 39d878bce5d..79816c693e2 100644 --- a/src/vs/base/test/common/event.test.ts +++ b/src/vs/base/test/common/event.test.ts @@ -623,6 +623,17 @@ suite('PausableEmitter', function () { assert.deepStrictEqual(data, [1, 1, 2, 2, 3, 3]); }); + + test('empty pause with merge', function () { + const data: number[] = []; + const emitter = new PauseableEmitter({ merge: a => a[0] }); + emitter.event(e => data.push(1)); + + emitter.pause(); + emitter.resume(); + assert.deepStrictEqual(data, []); + }); + }); suite('Event utils - ensureNoDisposablesAreLeakedInTestSuite', function () { diff --git a/src/vs/code/browser/workbench/workbench-dev.html b/src/vs/code/browser/workbench/workbench-dev.html index 7fdc35e0862..be6d30bfb98 100644 --- a/src/vs/code/browser/workbench/workbench-dev.html +++ b/src/vs/code/browser/workbench/workbench-dev.html @@ -28,6 +28,7 @@ + diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts index cad7de3f9ed..9f34c85cc2d 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts @@ -14,9 +14,13 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { request } from 'vs/base/parts/request/browser/request'; import product from 'vs/platform/product/common/product'; import { isFolderToOpen, isWorkspaceToOpen } from 'vs/platform/window/common/window'; -import { create, ICredentialsProvider, IURLCallbackProvider, IWorkbenchConstructionOptions, IWorkspace, IWorkspaceProvider } from 'vs/workbench/workbench.web.main'; +import { create } from 'vs/workbench/workbench.web.main'; import { posix } from 'vs/base/common/path'; import { ltrim } from 'vs/base/common/strings'; +import type { ICredentialsProvider } from 'vs/platform/credentials/common/credentials'; +import type { IURLCallbackProvider } from 'vs/workbench/services/url/browser/urlService'; +import type { IWorkbenchConstructionOptions } from 'vs/workbench/browser/web.api'; +import type { IWorkspace, IWorkspaceProvider } from 'vs/workbench/services/host/browser/browserHostService'; interface ICredential { service: string; diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcess-dev.html b/src/vs/code/electron-browser/sharedProcess/sharedProcess-dev.html new file mode 100644 index 00000000000..4bd9c876659 --- /dev/null +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcess-dev.html @@ -0,0 +1,18 @@ + + + + + + + + + + Shared Process + + + + + + + + diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcess.html b/src/vs/code/electron-browser/sharedProcess/sharedProcess.html index 07fd9bd0478..d1b5812fa76 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcess.html +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcess.html @@ -1,22 +1,15 @@ + + + + - - - - - - - Shared Process - - - - - - - - - + + Shared Process + + + diff --git a/src/vs/code/electron-sandbox/issue/issueReporter-dev.html b/src/vs/code/electron-sandbox/issue/issueReporter-dev.html new file mode 100644 index 00000000000..847f987ecc8 --- /dev/null +++ b/src/vs/code/electron-sandbox/issue/issueReporter-dev.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/vs/code/electron-sandbox/issue/issueReporter.html b/src/vs/code/electron-sandbox/issue/issueReporter.html index 3a0cb4be742..2f76d13ff86 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporter.html +++ b/src/vs/code/electron-sandbox/issue/issueReporter.html @@ -10,14 +10,10 @@ } + - - - - - - + diff --git a/src/vs/code/electron-sandbox/processExplorer/processExplorer-dev.html b/src/vs/code/electron-sandbox/processExplorer/processExplorer-dev.html new file mode 100644 index 00000000000..f6d8aa51604 --- /dev/null +++ b/src/vs/code/electron-sandbox/processExplorer/processExplorer-dev.html @@ -0,0 +1,18 @@ + + + + + + + + + +
+ + + + + + + + diff --git a/src/vs/code/electron-sandbox/processExplorer/processExplorer.html b/src/vs/code/electron-sandbox/processExplorer/processExplorer.html index 73d89eac31d..0eb6357a976 100644 --- a/src/vs/code/electron-sandbox/processExplorer/processExplorer.html +++ b/src/vs/code/electron-sandbox/processExplorer/processExplorer.html @@ -5,15 +5,11 @@ +
- - - - - - + diff --git a/src/vs/code/electron-sandbox/workbench/workbench-dev.html b/src/vs/code/electron-sandbox/workbench/workbench-dev.html new file mode 100644 index 00000000000..cccf92b4e6d --- /dev/null +++ b/src/vs/code/electron-sandbox/workbench/workbench-dev.html @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/vs/code/electron-sandbox/workbench/workbench.html b/src/vs/code/electron-sandbox/workbench/workbench.html index 16e089df650..ffee5aca3fb 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench.html +++ b/src/vs/code/electron-sandbox/workbench/workbench.html @@ -6,14 +6,10 @@ + - - - - - - + diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index daae8a10f16..e427bd7c6fb 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -334,7 +334,7 @@ export async function main(argv: string[]): Promise { return false; } if (target.type === 'page') { - return target.url.indexOf('workbench/workbench.html') > 0; + return target.url.indexOf('workbench/workbench.html') > 0 || target.url.indexOf('workbench/workbench-dev.html') > 0; } else { return true; } diff --git a/src/vs/editor/browser/controller/mouseHandler.ts b/src/vs/editor/browser/controller/mouseHandler.ts index 8b1bab406b2..bf3f85382cb 100644 --- a/src/vs/editor/browser/controller/mouseHandler.ts +++ b/src/vs/editor/browser/controller/mouseHandler.ts @@ -5,12 +5,11 @@ import * as dom from 'vs/base/browser/dom'; import { StandardWheelEvent, IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; -import { TimeoutTimer } from 'vs/base/common/async'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; import { HitTestContext, MouseTarget, MouseTargetFactory, PointerHandlerLastRenderData } from 'vs/editor/browser/controller/mouseTarget'; -import { IMouseTarget, IMouseTargetViewZoneData, MouseTargetType } from 'vs/editor/browser/editorBrowser'; -import { ClientCoordinates, EditorMouseEvent, EditorMouseEventFactory, GlobalEditorPointerMoveMonitor, createEditorPagePosition, createCoordinatesRelativeToEditor } from 'vs/editor/browser/editorDom'; +import { IMouseTarget, IMouseTargetOutsideEditor, IMouseTargetViewZoneData, MouseTargetType } from 'vs/editor/browser/editorBrowser'; +import { ClientCoordinates, EditorMouseEvent, EditorMouseEventFactory, GlobalEditorPointerMoveMonitor, createEditorPagePosition, createCoordinatesRelativeToEditor, PageCoordinates } from 'vs/editor/browser/editorDom'; import { ViewController } from 'vs/editor/browser/view/viewController'; import { EditorZoom } from 'vs/editor/common/config/editorZoom'; import { Position } from 'vs/editor/common/core/position'; @@ -20,6 +19,7 @@ import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; import * as viewEvents from 'vs/editor/common/viewEvents'; import { ViewEventHandler } from 'vs/editor/common/viewEventHandler'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { NavigationCommandRevealType } from 'vs/editor/browser/coreCommands'; export interface IPointerHandlerHelper { viewDomNode: HTMLElement; @@ -34,6 +34,11 @@ export interface IPointerHandlerHelper { */ getLastRenderData(): PointerHandlerLastRenderData; + /** + * Render right now + */ + renderNow(): void; + shouldSuppressMouseDownOnViewZone(viewZoneId: string): boolean; shouldSuppressMouseDownOnWidget(widgetId: string): boolean; @@ -69,6 +74,7 @@ export class MouseHandler extends ViewEventHandler { this._context, this.viewController, this.viewHelper, + this.mouseTargetFactory, (e, testEventTarget) => this._createMouseTarget(e, testEventTarget), (e) => this._getMouseColumn(e) )); @@ -177,10 +183,6 @@ export class MouseHandler extends ViewEventHandler { public override onFocusChanged(e: viewEvents.ViewFocusChangedEvent): boolean { return false; } - public override onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean { - this._mouseDownOperation.onScrollChanged(); - return false; - } // --- end event handlers public getTargetAtClientPoint(clientX: number, clientY: number): IMouseTarget | null { @@ -313,14 +315,11 @@ export class MouseHandler extends ViewEventHandler { class MouseDownOperation extends Disposable { - private readonly _context: ViewContext; - private readonly _viewController: ViewController; - private readonly _viewHelper: IPointerHandlerHelper; private readonly _createMouseTarget: (e: EditorMouseEvent, testEventTarget: boolean) => IMouseTarget; private readonly _getMouseColumn: (e: EditorMouseEvent) => number; private readonly _mouseMoveMonitor: GlobalEditorPointerMoveMonitor; - private readonly _onScrollTimeout: TimeoutTimer; + private readonly _topBottomDragScrolling: TopBottomDragScrolling; private readonly _mouseState: MouseDownState; private _currentSelection: Selection; @@ -328,21 +327,24 @@ class MouseDownOperation extends Disposable { private _lastMouseEvent: EditorMouseEvent | null; constructor( - context: ViewContext, - viewController: ViewController, - viewHelper: IPointerHandlerHelper, + private readonly _context: ViewContext, + private readonly _viewController: ViewController, + private readonly _viewHelper: IPointerHandlerHelper, + private readonly _mouseTargetFactory: MouseTargetFactory, createMouseTarget: (e: EditorMouseEvent, testEventTarget: boolean) => IMouseTarget, getMouseColumn: (e: EditorMouseEvent) => number ) { super(); - this._context = context; - this._viewController = viewController; - this._viewHelper = viewHelper; this._createMouseTarget = createMouseTarget; this._getMouseColumn = getMouseColumn; this._mouseMoveMonitor = this._register(new GlobalEditorPointerMoveMonitor(this._viewHelper.viewDomNode)); - this._onScrollTimeout = this._register(new TimeoutTimer()); + this._topBottomDragScrolling = this._register(new TopBottomDragScrolling( + this._context, + this._viewHelper, + this._mouseTargetFactory, + (position, inSelectionMode, revealType) => this._dispatchMouse(position, inSelectionMode, revealType) + )); this._mouseState = new MouseDownState(); this._currentSelection = new Selection(1, 1, 1, 1); @@ -374,7 +376,12 @@ class MouseDownOperation extends Disposable { target: position }); } else { - this._dispatchMouse(position, true); + if (position.type === MouseTargetType.OUTSIDE_EDITOR && (position.outsidePosition === 'above' || position.outsidePosition === 'below')) { + this._topBottomDragScrolling.start(position, e); + } else { + this._topBottomDragScrolling.stop(); + this._dispatchMouse(position, true, NavigationCommandRevealType.Minimal); + } } } @@ -436,7 +443,7 @@ class MouseDownOperation extends Disposable { } this._mouseState.isDragAndDrop = false; - this._dispatchMouse(position, e.shiftKey); + this._dispatchMouse(position, e.shiftKey, NavigationCommandRevealType.Minimal); if (!this._isActive) { this._isActive = true; @@ -452,7 +459,7 @@ class MouseDownOperation extends Disposable { private _stop(): void { this._isActive = false; - this._onScrollTimeout.cancel(); + this._topBottomDragScrolling.stop(); } public onHeightChanged(): void { @@ -463,27 +470,6 @@ class MouseDownOperation extends Disposable { this._mouseMoveMonitor.stopMonitoring(); } - public onScrollChanged(): void { - if (!this._isActive) { - return; - } - this._onScrollTimeout.setIfNotSet(() => { - if (!this._lastMouseEvent) { - return; - } - const position = this._findMousePosition(this._lastMouseEvent, false); - if (!position) { - // Ignoring because position is unknown - return; - } - if (this._mouseState.isDragAndDrop) { - // Ignoring because users are dragging the text - return; - } - this._dispatchMouse(position, true); - }, 10); - } - public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): void { this._currentSelection = e.selections[0]; } @@ -496,41 +482,45 @@ class MouseDownOperation extends Disposable { const mouseColumn = this._getMouseColumn(e); if (e.posy < editorContent.y) { - const verticalOffset = Math.max(viewLayout.getCurrentScrollTop() - (editorContent.y - e.posy), 0); + const outsideDistance = editorContent.y - e.posy; + const verticalOffset = Math.max(viewLayout.getCurrentScrollTop() - outsideDistance, 0); const viewZoneData = HitTestContext.getZoneAtCoord(this._context, verticalOffset); if (viewZoneData) { const newPosition = this._helpPositionJumpOverViewZone(viewZoneData); if (newPosition) { - return MouseTarget.createOutsideEditor(mouseColumn, newPosition); + return MouseTarget.createOutsideEditor(mouseColumn, newPosition, 'above', outsideDistance); } } const aboveLineNumber = viewLayout.getLineNumberAtVerticalOffset(verticalOffset); - return MouseTarget.createOutsideEditor(mouseColumn, new Position(aboveLineNumber, 1)); + return MouseTarget.createOutsideEditor(mouseColumn, new Position(aboveLineNumber, 1), 'above', outsideDistance); } if (e.posy > editorContent.y + editorContent.height) { + const outsideDistance = e.posy - editorContent.y - editorContent.height; const verticalOffset = viewLayout.getCurrentScrollTop() + e.relativePos.y; const viewZoneData = HitTestContext.getZoneAtCoord(this._context, verticalOffset); if (viewZoneData) { const newPosition = this._helpPositionJumpOverViewZone(viewZoneData); if (newPosition) { - return MouseTarget.createOutsideEditor(mouseColumn, newPosition); + return MouseTarget.createOutsideEditor(mouseColumn, newPosition, 'below', outsideDistance); } } const belowLineNumber = viewLayout.getLineNumberAtVerticalOffset(verticalOffset); - return MouseTarget.createOutsideEditor(mouseColumn, new Position(belowLineNumber, model.getLineMaxColumn(belowLineNumber))); + return MouseTarget.createOutsideEditor(mouseColumn, new Position(belowLineNumber, model.getLineMaxColumn(belowLineNumber)), 'below', outsideDistance); } const possibleLineNumber = viewLayout.getLineNumberAtVerticalOffset(viewLayout.getCurrentScrollTop() + e.relativePos.y); if (e.posx < editorContent.x) { - return MouseTarget.createOutsideEditor(mouseColumn, new Position(possibleLineNumber, 1)); + const outsideDistance = editorContent.x - e.posx; + return MouseTarget.createOutsideEditor(mouseColumn, new Position(possibleLineNumber, 1), 'left', outsideDistance); } if (e.posx > editorContent.x + editorContent.width) { - return MouseTarget.createOutsideEditor(mouseColumn, new Position(possibleLineNumber, model.getLineMaxColumn(possibleLineNumber))); + const outsideDistance = e.posx - editorContent.x - editorContent.width; + return MouseTarget.createOutsideEditor(mouseColumn, new Position(possibleLineNumber, model.getLineMaxColumn(possibleLineNumber)), 'right', outsideDistance); } return null; @@ -574,7 +564,7 @@ class MouseDownOperation extends Disposable { return null; } - private _dispatchMouse(position: IMouseTarget, inSelectionMode: boolean): void { + private _dispatchMouse(position: IMouseTarget, inSelectionMode: boolean, revealType: NavigationCommandRevealType): void { if (!position.position) { return; } @@ -582,6 +572,7 @@ class MouseDownOperation extends Disposable { position: position.position, mouseColumn: position.mouseColumn, startedOnLineNumbers: this._mouseState.startedOnLineNumbers, + revealType, inSelectionMode: inSelectionMode, mouseDownCount: this._mouseState.count, @@ -598,6 +589,134 @@ class MouseDownOperation extends Disposable { } } +class TopBottomDragScrolling extends Disposable { + + private _operation: TopBottomDragScrollingOperation | null; + + constructor( + private readonly _context: ViewContext, + private readonly _viewHelper: IPointerHandlerHelper, + private readonly _mouseTargetFactory: MouseTargetFactory, + private readonly _dispatchMouse: (position: IMouseTarget, inSelectionMode: boolean, revealType: NavigationCommandRevealType) => void, + ) { + super(); + this._operation = null; + } + + public override dispose(): void { + super.dispose(); + this.stop(); + } + + public start(position: IMouseTargetOutsideEditor, mouseEvent: EditorMouseEvent): void { + if (this._operation) { + this._operation.setPosition(position, mouseEvent); + } else { + this._operation = new TopBottomDragScrollingOperation(this._context, this._viewHelper, this._mouseTargetFactory, this._dispatchMouse, position, mouseEvent); + } + } + + public stop(): void { + if (this._operation) { + this._operation.dispose(); + this._operation = null; + } + } +} + +class TopBottomDragScrollingOperation extends Disposable { + + private _position: IMouseTargetOutsideEditor; + private _mouseEvent: EditorMouseEvent; + private _lastTime: number; + private _animationFrameDisposable: IDisposable; + + constructor( + private readonly _context: ViewContext, + private readonly _viewHelper: IPointerHandlerHelper, + private readonly _mouseTargetFactory: MouseTargetFactory, + private readonly _dispatchMouse: (position: IMouseTarget, inSelectionMode: boolean, revealType: NavigationCommandRevealType) => void, + position: IMouseTargetOutsideEditor, + mouseEvent: EditorMouseEvent + ) { + super(); + this._position = position; + this._mouseEvent = mouseEvent; + this._lastTime = Date.now(); + this._animationFrameDisposable = dom.scheduleAtNextAnimationFrame(() => this._execute()); + } + + public override dispose(): void { + this._animationFrameDisposable.dispose(); + } + + public setPosition(position: IMouseTargetOutsideEditor, mouseEvent: EditorMouseEvent): void { + this._position = position; + this._mouseEvent = mouseEvent; + } + + /** + * update internal state and return elapsed ms since last time + */ + private _tick(): number { + const now = Date.now(); + const elapsed = now - this._lastTime; + this._lastTime = now; + return elapsed; + } + + /** + * get the number of lines per second to auto-scroll + */ + private _getScrollSpeed(): number { + const lineHeight = this._context.configuration.options.get(EditorOption.lineHeight); + const viewportInLines = this._context.configuration.options.get(EditorOption.layoutInfo).height / lineHeight; + const outsideDistanceInLines = this._position.outsideDistance / lineHeight; + + if (outsideDistanceInLines <= 1.5) { + return Math.max(30, viewportInLines * (1 + outsideDistanceInLines)); + } + if (outsideDistanceInLines <= 3) { + return Math.max(60, viewportInLines * (2 + outsideDistanceInLines)); + } + return Math.max(200, viewportInLines * (7 + outsideDistanceInLines)); + } + + private _execute(): void { + const lineHeight = this._context.configuration.options.get(EditorOption.lineHeight); + const scrollSpeedInLines = this._getScrollSpeed(); + const elapsed = this._tick(); + const scrollInPixels = scrollSpeedInLines * (elapsed / 1000) * lineHeight; + const scrollValue = (this._position.outsidePosition === 'above' ? -scrollInPixels : scrollInPixels); + + this._context.viewModel.viewLayout.deltaScrollNow(0, scrollValue); + this._viewHelper.renderNow(); + + const viewportData = this._context.viewLayout.getLinesViewportData(); + const edgeLineNumber = (this._position.outsidePosition === 'above' ? viewportData.startLineNumber : viewportData.endLineNumber); + + // First, try to find a position that matches the horizontal position of the mouse + let mouseTarget: IMouseTarget; + { + const editorPos = createEditorPagePosition(this._viewHelper.viewDomNode); + const horizontalScrollbarHeight = this._context.configuration.options.get(EditorOption.layoutInfo).horizontalScrollbarHeight; + const pos = new PageCoordinates(this._mouseEvent.pos.x, editorPos.y + editorPos.height - horizontalScrollbarHeight - 0.1); + const relativePos = createCoordinatesRelativeToEditor(this._viewHelper.viewDomNode, editorPos, pos); + mouseTarget = this._mouseTargetFactory.createMouseTarget(this._viewHelper.getLastRenderData(), editorPos, pos, relativePos, null); + } + if (!mouseTarget.position || mouseTarget.position.lineNumber !== edgeLineNumber) { + if (this._position.outsidePosition === 'above') { + mouseTarget = MouseTarget.createOutsideEditor(this._position.mouseColumn, new Position(edgeLineNumber, 1), 'above', this._position.outsideDistance); + } else { + mouseTarget = MouseTarget.createOutsideEditor(this._position.mouseColumn, new Position(edgeLineNumber, this._context.viewModel.getLineMaxColumn(edgeLineNumber)), 'below', this._position.outsideDistance); + } + } + + this._dispatchMouse(mouseTarget, true, NavigationCommandRevealType.None); + this._animationFrameDisposable = dom.scheduleAtNextAnimationFrame(() => this._execute()); + } +} + class MouseDownState { private static readonly CLEAR_MOUSE_DOWN_COUNT_TIME = 400; // ms diff --git a/src/vs/editor/browser/controller/mouseTarget.ts b/src/vs/editor/browser/controller/mouseTarget.ts index c13cd539659..beca450d9ef 100644 --- a/src/vs/editor/browser/controller/mouseTarget.ts +++ b/src/vs/editor/browser/controller/mouseTarget.ts @@ -99,8 +99,8 @@ export class MouseTarget { public static createOverlayWidget(element: Element | null, mouseColumn: number, detail: string): IMouseTargetOverlayWidget { return { type: MouseTargetType.OVERLAY_WIDGET, element, mouseColumn, position: null, range: null, detail }; } - public static createOutsideEditor(mouseColumn: number, position: Position): IMouseTargetOutsideEditor { - return { type: MouseTargetType.OUTSIDE_EDITOR, element: null, mouseColumn, position, range: this._deduceRage(position) }; + public static createOutsideEditor(mouseColumn: number, position: Position, outsidePosition: 'above' | 'below' | 'left' | 'right', outsideDistance: number): IMouseTargetOutsideEditor { + return { type: MouseTargetType.OUTSIDE_EDITOR, element: null, mouseColumn, position, range: this._deduceRage(position), outsidePosition, outsideDistance }; } private static _typeToString(type: MouseTargetType): string { diff --git a/src/vs/editor/browser/controller/pointerHandler.ts b/src/vs/editor/browser/controller/pointerHandler.ts index 1fd1525a4c2..25155f1336d 100644 --- a/src/vs/editor/browser/controller/pointerHandler.ts +++ b/src/vs/editor/browser/controller/pointerHandler.ts @@ -14,6 +14,7 @@ import { ViewController } from 'vs/editor/browser/view/viewController'; import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; import { BrowserFeatures } from 'vs/base/browser/canIUse'; import { TextAreaSyntethicEvents } from 'vs/editor/browser/controller/textAreaInput'; +import { NavigationCommandRevealType } from 'vs/editor/browser/coreCommands'; /** * Currently only tested on iOS 13/ iPadOS. @@ -66,6 +67,7 @@ export class PointerEventHandler extends MouseHandler { position: target.position, mouseColumn: target.position.column, startedOnLineNumbers: false, + revealType: NavigationCommandRevealType.Minimal, mouseDownCount: event.tapCount, inSelectionMode: false, altKey: false, @@ -120,7 +122,7 @@ class TouchHandler extends MouseHandler { event.initEvent(TextAreaSyntethicEvents.Tap, false, true); this.viewHelper.dispatchTextAreaEvent(event); - this.viewController.moveTo(target.position); + this.viewController.moveTo(target.position, NavigationCommandRevealType.Minimal); } } diff --git a/src/vs/editor/browser/coreCommands.ts b/src/vs/editor/browser/coreCommands.ts index b5ab2a699e1..6fa113c75ee 100644 --- a/src/vs/editor/browser/coreCommands.ts +++ b/src/vs/editor/browser/coreCommands.ts @@ -17,7 +17,7 @@ import { DeleteOperations } from 'vs/editor/common/cursor/cursorDeleteOperations import { CursorChangeReason } from 'vs/editor/common/cursorEvents'; import { CursorMove as CursorMove_, CursorMoveCommands } from 'vs/editor/common/cursor/cursorMoveCommands'; import { TypeOperations } from 'vs/editor/common/cursor/cursorTypeOperations'; -import { Position } from 'vs/editor/common/core/position'; +import { IPosition, Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Handler, ScrollType } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; @@ -28,11 +28,12 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { KeybindingWeight, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IViewModel } from 'vs/editor/common/viewModel'; +import { ISelection } from 'vs/editor/common/core/selection'; const CORE_WEIGHT = KeybindingWeight.EditorCore; -export abstract class CoreEditorCommand extends EditorCommand { - public runEditorCommand(accessor: ServicesAccessor | null, editor: ICodeEditor, args: any): void { +export abstract class CoreEditorCommand extends EditorCommand { + public runEditorCommand(accessor: ServicesAccessor | null, editor: ICodeEditor, args?: Partial | null): void { const viewModel = editor._getViewModel(); if (!viewModel) { // the editor has no view => has no cursors @@ -41,7 +42,7 @@ export abstract class CoreEditorCommand extends EditorCommand { this.runCoreEditorCommand(viewModel, args || {}); } - public abstract runCoreEditorCommand(viewModel: IViewModel, args: any): void; + public abstract runCoreEditorCommand(viewModel: IViewModel, args: Partial): void; } export namespace EditorScroll_ { @@ -145,7 +146,7 @@ export namespace EditorScroll_ { select?: boolean; } - export function parse(args: RawArguments): ParsedArguments | null { + export function parse(args: Partial): ParsedArguments | null { let direction: Direction; switch (args.to) { case RawDirection.Up: @@ -286,7 +287,7 @@ abstract class EditorOrNativeTextInputCommand { constructor(target: MultiCommand) { // 1. handle case when focus is in editor. - target.addImplementation(10000, 'code-editor', (accessor: ServicesAccessor, args: any) => { + target.addImplementation(10000, 'code-editor', (accessor: ServicesAccessor, args: unknown) => { // Only if editor text focus (i.e. not if editor has widget focus). const focusedEditor = accessor.get(ICodeEditorService).getFocusedCodeEditor(); if (focusedEditor && focusedEditor.hasTextFocus()) { @@ -296,7 +297,7 @@ abstract class EditorOrNativeTextInputCommand { }); // 2. handle case when focus is in some other `input` / `textarea`. - target.addImplementation(1000, 'generic-dom-input-textarea', (accessor: ServicesAccessor, args: any) => { + target.addImplementation(1000, 'generic-dom-input-textarea', (accessor: ServicesAccessor, args: unknown) => { // Only if focused on an element that allows for entering text const activeElement = document.activeElement; if (activeElement && ['input', 'textarea'].indexOf(activeElement.tagName.toLowerCase()) >= 0) { @@ -307,7 +308,7 @@ abstract class EditorOrNativeTextInputCommand { }); // 3. (default) handle case when focus is somewhere else. - target.addImplementation(0, 'generic-dom', (accessor: ServicesAccessor, args: any) => { + target.addImplementation(0, 'generic-dom', (accessor: ServicesAccessor, args: unknown) => { // Redirecting to active editor const activeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor(); if (activeEditor) { @@ -318,7 +319,7 @@ abstract class EditorOrNativeTextInputCommand { }); } - public _runEditorCommand(accessor: ServicesAccessor | null, editor: ICodeEditor, args: any): boolean | Promise { + public _runEditorCommand(accessor: ServicesAccessor | null, editor: ICodeEditor, args: unknown): boolean | Promise { const result = this.runEditorCommand(accessor, editor, args); if (result) { return result; @@ -327,23 +328,49 @@ abstract class EditorOrNativeTextInputCommand { } public abstract runDOMCommand(): void; - public abstract runEditorCommand(accessor: ServicesAccessor | null, editor: ICodeEditor, args: any): void | Promise; + public abstract runEditorCommand(accessor: ServicesAccessor | null, editor: ICodeEditor, args: unknown): void | Promise; +} + +export const enum NavigationCommandRevealType { + /** + * Do regular revealing. + */ + Regular = 0, + /** + * Do only minimal revealing. + */ + Minimal = 1, + /** + * Do not reveal the position. + */ + None = 2 } export namespace CoreNavigationCommands { - class BaseMoveToCommand extends CoreEditorCommand { + export interface BaseCommandOptions { + source?: 'mouse' | 'keyboard' | string; + } + + export interface MoveCommandOptions extends BaseCommandOptions { + position: IPosition; + viewPosition?: IPosition; + revealType: NavigationCommandRevealType; + } + + class BaseMoveToCommand extends CoreEditorCommand { - private readonly _minimalReveal: boolean; private readonly _inSelectionMode: boolean; - constructor(opts: ICommandOptions & { minimalReveal: boolean; inSelectionMode: boolean }) { + constructor(opts: ICommandOptions & { inSelectionMode: boolean }) { super(opts); - this._minimalReveal = opts.minimalReveal; this._inSelectionMode = opts.inSelectionMode; } - public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + public runCoreEditorCommand(viewModel: IViewModel, args: Partial): void { + if (!args.position) { + return; + } viewModel.model.pushStackElement(); const cursorStateChanged = viewModel.setCursorStates( args.source, @@ -352,30 +379,32 @@ export namespace CoreNavigationCommands { CursorMoveCommands.moveTo(viewModel, viewModel.getPrimaryCursorState(), this._inSelectionMode, args.position, args.viewPosition) ] ); - if (cursorStateChanged) { - viewModel.revealPrimaryCursor(args.source, true, this._minimalReveal); + if (cursorStateChanged && args.revealType !== NavigationCommandRevealType.None) { + viewModel.revealPrimaryCursor(args.source, true, true); } } } - export const MoveTo: CoreEditorCommand = registerEditorCommand(new BaseMoveToCommand({ + export const MoveTo: CoreEditorCommand = registerEditorCommand(new BaseMoveToCommand({ id: '_moveTo', - minimalReveal: true, inSelectionMode: false, precondition: undefined })); - export const MoveToSelect: CoreEditorCommand = registerEditorCommand(new BaseMoveToCommand({ + export const MoveToSelect: CoreEditorCommand = registerEditorCommand(new BaseMoveToCommand({ id: '_moveToSelect', - minimalReveal: false, inSelectionMode: true, precondition: undefined })); - abstract class ColumnSelectCommand extends CoreEditorCommand { - public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + abstract class ColumnSelectCommand extends CoreEditorCommand { + public runCoreEditorCommand(viewModel: IViewModel, args: Partial): void { viewModel.model.pushStackElement(); const result = this._getColumnSelectResult(viewModel, viewModel.getPrimaryCursorState(), viewModel.getCursorColumnSelectData(), args); + if (result === null) { + // invalid arguments + return; + } viewModel.setCursorStates(args.source, CursorChangeReason.Explicit, result.viewStates.map((viewState) => CursorState.fromViewState(viewState))); viewModel.setCursorColumnSelectData({ isReal: true, @@ -391,11 +420,18 @@ export namespace CoreNavigationCommands { } } - protected abstract _getColumnSelectResult(viewModel: IViewModel, primary: CursorState, prevColumnSelectData: IColumnSelectData, args: any): IColumnSelectResult; + protected abstract _getColumnSelectResult(viewModel: IViewModel, primary: CursorState, prevColumnSelectData: IColumnSelectData, args: Partial): IColumnSelectResult | null; } - export const ColumnSelect: CoreEditorCommand = registerEditorCommand(new class extends ColumnSelectCommand { + export interface ColumnSelectCommandOptions extends BaseCommandOptions { + position: IPosition; + viewPosition: IPosition; + mouseColumn: number; + doColumnSelect: boolean; + } + + export const ColumnSelect: CoreEditorCommand = registerEditorCommand(new class extends ColumnSelectCommand { constructor() { super({ id: 'columnSelect', @@ -403,8 +439,10 @@ export namespace CoreNavigationCommands { }); } - protected _getColumnSelectResult(viewModel: IViewModel, primary: CursorState, prevColumnSelectData: IColumnSelectData, args: any): IColumnSelectResult { - + protected _getColumnSelectResult(viewModel: IViewModel, primary: CursorState, prevColumnSelectData: IColumnSelectData, args: Partial): IColumnSelectResult | null { + if (typeof args.position === 'undefined' || typeof args.viewPosition === 'undefined' || typeof args.mouseColumn === 'undefined') { + return null; + } // validate `args` const validatedPosition = viewModel.model.validatePosition(args.position); const validatedViewPosition = viewModel.coordinatesConverter.validateViewPosition(new Position(args.viewPosition.lineNumber, args.viewPosition.column), validatedPosition); @@ -415,7 +453,7 @@ export namespace CoreNavigationCommands { } }); - export const CursorColumnSelectLeft: CoreEditorCommand = registerEditorCommand(new class extends ColumnSelectCommand { + export const CursorColumnSelectLeft: CoreEditorCommand = registerEditorCommand(new class extends ColumnSelectCommand { constructor() { super({ id: 'cursorColumnSelectLeft', @@ -429,12 +467,12 @@ export namespace CoreNavigationCommands { }); } - protected _getColumnSelectResult(viewModel: IViewModel, primary: CursorState, prevColumnSelectData: IColumnSelectData, args: any): IColumnSelectResult { + protected _getColumnSelectResult(viewModel: IViewModel, primary: CursorState, prevColumnSelectData: IColumnSelectData, args: Partial): IColumnSelectResult { return ColumnSelection.columnSelectLeft(viewModel.cursorConfig, viewModel, prevColumnSelectData); } }); - export const CursorColumnSelectRight: CoreEditorCommand = registerEditorCommand(new class extends ColumnSelectCommand { + export const CursorColumnSelectRight: CoreEditorCommand = registerEditorCommand(new class extends ColumnSelectCommand { constructor() { super({ id: 'cursorColumnSelectRight', @@ -448,7 +486,7 @@ export namespace CoreNavigationCommands { }); } - protected _getColumnSelectResult(viewModel: IViewModel, primary: CursorState, prevColumnSelectData: IColumnSelectData, args: any): IColumnSelectResult { + protected _getColumnSelectResult(viewModel: IViewModel, primary: CursorState, prevColumnSelectData: IColumnSelectData, args: Partial): IColumnSelectResult { return ColumnSelection.columnSelectRight(viewModel.cursorConfig, viewModel, prevColumnSelectData); } }); @@ -462,12 +500,12 @@ export namespace CoreNavigationCommands { this._isPaged = opts.isPaged; } - protected _getColumnSelectResult(viewModel: IViewModel, primary: CursorState, prevColumnSelectData: IColumnSelectData, args: any): IColumnSelectResult { + protected _getColumnSelectResult(viewModel: IViewModel, primary: CursorState, prevColumnSelectData: IColumnSelectData, args: Partial): IColumnSelectResult { return ColumnSelection.columnSelectUp(viewModel.cursorConfig, viewModel, prevColumnSelectData, this._isPaged); } } - export const CursorColumnSelectUp: CoreEditorCommand = registerEditorCommand(new ColumnSelectUpCommand({ + export const CursorColumnSelectUp: CoreEditorCommand = registerEditorCommand(new ColumnSelectUpCommand({ isPaged: false, id: 'cursorColumnSelectUp', precondition: undefined, @@ -479,7 +517,7 @@ export namespace CoreNavigationCommands { } })); - export const CursorColumnSelectPageUp: CoreEditorCommand = registerEditorCommand(new ColumnSelectUpCommand({ + export const CursorColumnSelectPageUp: CoreEditorCommand = registerEditorCommand(new ColumnSelectUpCommand({ isPaged: true, id: 'cursorColumnSelectPageUp', precondition: undefined, @@ -500,12 +538,12 @@ export namespace CoreNavigationCommands { this._isPaged = opts.isPaged; } - protected _getColumnSelectResult(viewModel: IViewModel, primary: CursorState, prevColumnSelectData: IColumnSelectData, args: any): IColumnSelectResult { + protected _getColumnSelectResult(viewModel: IViewModel, primary: CursorState, prevColumnSelectData: IColumnSelectData, args: Partial): IColumnSelectResult { return ColumnSelection.columnSelectDown(viewModel.cursorConfig, viewModel, prevColumnSelectData, this._isPaged); } } - export const CursorColumnSelectDown: CoreEditorCommand = registerEditorCommand(new ColumnSelectDownCommand({ + export const CursorColumnSelectDown: CoreEditorCommand = registerEditorCommand(new ColumnSelectDownCommand({ isPaged: false, id: 'cursorColumnSelectDown', precondition: undefined, @@ -517,7 +555,7 @@ export namespace CoreNavigationCommands { } })); - export const CursorColumnSelectPageDown: CoreEditorCommand = registerEditorCommand(new ColumnSelectDownCommand({ + export const CursorColumnSelectPageDown: CoreEditorCommand = registerEditorCommand(new ColumnSelectDownCommand({ isPaged: true, id: 'cursorColumnSelectPageDown', precondition: undefined, @@ -529,7 +567,7 @@ export namespace CoreNavigationCommands { } })); - export class CursorMoveImpl extends CoreEditorCommand { + export class CursorMoveImpl extends CoreEditorCommand { constructor() { super({ id: 'cursorMove', @@ -538,7 +576,7 @@ export namespace CoreNavigationCommands { }); } - public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + public runCoreEditorCommand(viewModel: IViewModel, args: Partial): void { const parsed = CursorMove_.parse(args); if (!parsed) { // illegal arguments @@ -592,7 +630,11 @@ export namespace CoreNavigationCommands { PAGE_SIZE_MARKER = -1 } - class CursorMoveBasedCommand extends CoreEditorCommand { + export interface CursorMoveCommandOptions extends BaseCommandOptions { + pageSize?: number; + } + + class CursorMoveBasedCommand extends CoreEditorCommand { private readonly _staticArgs: CursorMove_.SimpleMoveArguments; @@ -601,7 +643,7 @@ export namespace CoreNavigationCommands { this._staticArgs = opts.args; } - public runCoreEditorCommand(viewModel: IViewModel, dynamicArgs: any): void { + public runCoreEditorCommand(viewModel: IViewModel, dynamicArgs: Partial): void { let args = this._staticArgs; if (this._staticArgs.value === Constants.PAGE_SIZE_MARKER) { // -1 is a marker for page size @@ -623,7 +665,7 @@ export namespace CoreNavigationCommands { } } - export const CursorLeft: CoreEditorCommand = registerEditorCommand(new CursorMoveBasedCommand({ + export const CursorLeft: CoreEditorCommand = registerEditorCommand(new CursorMoveBasedCommand({ args: { direction: CursorMove_.Direction.Left, unit: CursorMove_.Unit.None, @@ -640,7 +682,7 @@ export namespace CoreNavigationCommands { } })); - export const CursorLeftSelect: CoreEditorCommand = registerEditorCommand(new CursorMoveBasedCommand({ + export const CursorLeftSelect: CoreEditorCommand = registerEditorCommand(new CursorMoveBasedCommand({ args: { direction: CursorMove_.Direction.Left, unit: CursorMove_.Unit.None, @@ -656,7 +698,7 @@ export namespace CoreNavigationCommands { } })); - export const CursorRight: CoreEditorCommand = registerEditorCommand(new CursorMoveBasedCommand({ + export const CursorRight: CoreEditorCommand = registerEditorCommand(new CursorMoveBasedCommand({ args: { direction: CursorMove_.Direction.Right, unit: CursorMove_.Unit.None, @@ -673,7 +715,7 @@ export namespace CoreNavigationCommands { } })); - export const CursorRightSelect: CoreEditorCommand = registerEditorCommand(new CursorMoveBasedCommand({ + export const CursorRightSelect: CoreEditorCommand = registerEditorCommand(new CursorMoveBasedCommand({ args: { direction: CursorMove_.Direction.Right, unit: CursorMove_.Unit.None, @@ -689,7 +731,7 @@ export namespace CoreNavigationCommands { } })); - export const CursorUp: CoreEditorCommand = registerEditorCommand(new CursorMoveBasedCommand({ + export const CursorUp: CoreEditorCommand = registerEditorCommand(new CursorMoveBasedCommand({ args: { direction: CursorMove_.Direction.Up, unit: CursorMove_.Unit.WrappedLine, @@ -706,7 +748,7 @@ export namespace CoreNavigationCommands { } })); - export const CursorUpSelect: CoreEditorCommand = registerEditorCommand(new CursorMoveBasedCommand({ + export const CursorUpSelect: CoreEditorCommand = registerEditorCommand(new CursorMoveBasedCommand({ args: { direction: CursorMove_.Direction.Up, unit: CursorMove_.Unit.WrappedLine, @@ -725,7 +767,7 @@ export namespace CoreNavigationCommands { } })); - export const CursorPageUp: CoreEditorCommand = registerEditorCommand(new CursorMoveBasedCommand({ + export const CursorPageUp: CoreEditorCommand = registerEditorCommand(new CursorMoveBasedCommand({ args: { direction: CursorMove_.Direction.Up, unit: CursorMove_.Unit.WrappedLine, @@ -741,7 +783,7 @@ export namespace CoreNavigationCommands { } })); - export const CursorPageUpSelect: CoreEditorCommand = registerEditorCommand(new CursorMoveBasedCommand({ + export const CursorPageUpSelect: CoreEditorCommand = registerEditorCommand(new CursorMoveBasedCommand({ args: { direction: CursorMove_.Direction.Up, unit: CursorMove_.Unit.WrappedLine, @@ -757,7 +799,7 @@ export namespace CoreNavigationCommands { } })); - export const CursorDown: CoreEditorCommand = registerEditorCommand(new CursorMoveBasedCommand({ + export const CursorDown: CoreEditorCommand = registerEditorCommand(new CursorMoveBasedCommand({ args: { direction: CursorMove_.Direction.Down, unit: CursorMove_.Unit.WrappedLine, @@ -774,7 +816,7 @@ export namespace CoreNavigationCommands { } })); - export const CursorDownSelect: CoreEditorCommand = registerEditorCommand(new CursorMoveBasedCommand({ + export const CursorDownSelect: CoreEditorCommand = registerEditorCommand(new CursorMoveBasedCommand({ args: { direction: CursorMove_.Direction.Down, unit: CursorMove_.Unit.WrappedLine, @@ -793,7 +835,7 @@ export namespace CoreNavigationCommands { } })); - export const CursorPageDown: CoreEditorCommand = registerEditorCommand(new CursorMoveBasedCommand({ + export const CursorPageDown: CoreEditorCommand = registerEditorCommand(new CursorMoveBasedCommand({ args: { direction: CursorMove_.Direction.Down, unit: CursorMove_.Unit.WrappedLine, @@ -809,7 +851,7 @@ export namespace CoreNavigationCommands { } })); - export const CursorPageDownSelect: CoreEditorCommand = registerEditorCommand(new CursorMoveBasedCommand({ + export const CursorPageDownSelect: CoreEditorCommand = registerEditorCommand(new CursorMoveBasedCommand({ args: { direction: CursorMove_.Direction.Down, unit: CursorMove_.Unit.WrappedLine, @@ -825,7 +867,11 @@ export namespace CoreNavigationCommands { } })); - export const CreateCursor: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { + export interface CreateCursorCommandOptions extends MoveCommandOptions { + wholeLine?: boolean; + } + + export const CreateCursor: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { constructor() { super({ id: 'createCursor', @@ -833,7 +879,10 @@ export namespace CoreNavigationCommands { }); } - public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + public runCoreEditorCommand(viewModel: IViewModel, args: Partial): void { + if (!args.position) { + return; + } let newState: PartialCursorState; if (args.wholeLine) { newState = CursorMoveCommands.line(viewModel, viewModel.getPrimaryCursorState(), false, args.position, args.viewPosition); @@ -884,7 +933,7 @@ export namespace CoreNavigationCommands { } }); - export const LastCursorMoveToSelect: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { + export const LastCursorMoveToSelect: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { constructor() { super({ id: '_lastCursorMoveToSelect', @@ -892,7 +941,10 @@ export namespace CoreNavigationCommands { }); } - public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + public runCoreEditorCommand(viewModel: IViewModel, args: Partial): void { + if (!args.position) { + return; + } const lastAddedCursorIndex = viewModel.getLastAddedCursorIndex(); const states = viewModel.getCursorStates(); @@ -908,7 +960,7 @@ export namespace CoreNavigationCommands { } }); - class HomeCommand extends CoreEditorCommand { + class HomeCommand extends CoreEditorCommand { private readonly _inSelectionMode: boolean; @@ -917,7 +969,7 @@ export namespace CoreNavigationCommands { this._inSelectionMode = opts.inSelectionMode; } - public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + public runCoreEditorCommand(viewModel: IViewModel, args: Partial): void { viewModel.model.pushStackElement(); viewModel.setCursorStates( args.source, @@ -928,7 +980,7 @@ export namespace CoreNavigationCommands { } } - export const CursorHome: CoreEditorCommand = registerEditorCommand(new HomeCommand({ + export const CursorHome: CoreEditorCommand = registerEditorCommand(new HomeCommand({ inSelectionMode: false, id: 'cursorHome', precondition: undefined, @@ -940,7 +992,7 @@ export namespace CoreNavigationCommands { } })); - export const CursorHomeSelect: CoreEditorCommand = registerEditorCommand(new HomeCommand({ + export const CursorHomeSelect: CoreEditorCommand = registerEditorCommand(new HomeCommand({ inSelectionMode: true, id: 'cursorHomeSelect', precondition: undefined, @@ -952,7 +1004,7 @@ export namespace CoreNavigationCommands { } })); - class LineStartCommand extends CoreEditorCommand { + class LineStartCommand extends CoreEditorCommand { private readonly _inSelectionMode: boolean; @@ -961,7 +1013,7 @@ export namespace CoreNavigationCommands { this._inSelectionMode = opts.inSelectionMode; } - public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + public runCoreEditorCommand(viewModel: IViewModel, args: Partial): void { viewModel.model.pushStackElement(); viewModel.setCursorStates( args.source, @@ -982,7 +1034,7 @@ export namespace CoreNavigationCommands { } } - export const CursorLineStart: CoreEditorCommand = registerEditorCommand(new LineStartCommand({ + export const CursorLineStart: CoreEditorCommand = registerEditorCommand(new LineStartCommand({ inSelectionMode: false, id: 'cursorLineStart', precondition: undefined, @@ -994,7 +1046,7 @@ export namespace CoreNavigationCommands { } })); - export const CursorLineStartSelect: CoreEditorCommand = registerEditorCommand(new LineStartCommand({ + export const CursorLineStartSelect: CoreEditorCommand = registerEditorCommand(new LineStartCommand({ inSelectionMode: true, id: 'cursorLineStartSelect', precondition: undefined, @@ -1006,7 +1058,11 @@ export namespace CoreNavigationCommands { } })); - class EndCommand extends CoreEditorCommand { + export interface EndCommandOptions extends BaseCommandOptions { + sticky?: boolean; + } + + class EndCommand extends CoreEditorCommand { private readonly _inSelectionMode: boolean; @@ -1015,7 +1071,7 @@ export namespace CoreNavigationCommands { this._inSelectionMode = opts.inSelectionMode; } - public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + public runCoreEditorCommand(viewModel: IViewModel, args: Partial): void { viewModel.model.pushStackElement(); viewModel.setCursorStates( args.source, @@ -1026,7 +1082,7 @@ export namespace CoreNavigationCommands { } } - export const CursorEnd: CoreEditorCommand = registerEditorCommand(new EndCommand({ + export const CursorEnd: CoreEditorCommand = registerEditorCommand(new EndCommand({ inSelectionMode: false, id: 'cursorEnd', precondition: undefined, @@ -1055,7 +1111,7 @@ export namespace CoreNavigationCommands { } })); - export const CursorEndSelect: CoreEditorCommand = registerEditorCommand(new EndCommand({ + export const CursorEndSelect: CoreEditorCommand = registerEditorCommand(new EndCommand({ inSelectionMode: true, id: 'cursorEndSelect', precondition: undefined, @@ -1084,7 +1140,7 @@ export namespace CoreNavigationCommands { } })); - class LineEndCommand extends CoreEditorCommand { + class LineEndCommand extends CoreEditorCommand { private readonly _inSelectionMode: boolean; @@ -1093,7 +1149,7 @@ export namespace CoreNavigationCommands { this._inSelectionMode = opts.inSelectionMode; } - public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + public runCoreEditorCommand(viewModel: IViewModel, args: Partial): void { viewModel.model.pushStackElement(); viewModel.setCursorStates( args.source, @@ -1115,7 +1171,7 @@ export namespace CoreNavigationCommands { } } - export const CursorLineEnd: CoreEditorCommand = registerEditorCommand(new LineEndCommand({ + export const CursorLineEnd: CoreEditorCommand = registerEditorCommand(new LineEndCommand({ inSelectionMode: false, id: 'cursorLineEnd', precondition: undefined, @@ -1127,7 +1183,7 @@ export namespace CoreNavigationCommands { } })); - export const CursorLineEndSelect: CoreEditorCommand = registerEditorCommand(new LineEndCommand({ + export const CursorLineEndSelect: CoreEditorCommand = registerEditorCommand(new LineEndCommand({ inSelectionMode: true, id: 'cursorLineEndSelect', precondition: undefined, @@ -1139,7 +1195,7 @@ export namespace CoreNavigationCommands { } })); - class TopCommand extends CoreEditorCommand { + class TopCommand extends CoreEditorCommand { private readonly _inSelectionMode: boolean; @@ -1148,7 +1204,7 @@ export namespace CoreNavigationCommands { this._inSelectionMode = opts.inSelectionMode; } - public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + public runCoreEditorCommand(viewModel: IViewModel, args: Partial): void { viewModel.model.pushStackElement(); viewModel.setCursorStates( args.source, @@ -1159,7 +1215,7 @@ export namespace CoreNavigationCommands { } } - export const CursorTop: CoreEditorCommand = registerEditorCommand(new TopCommand({ + export const CursorTop: CoreEditorCommand = registerEditorCommand(new TopCommand({ inSelectionMode: false, id: 'cursorTop', precondition: undefined, @@ -1171,7 +1227,7 @@ export namespace CoreNavigationCommands { } })); - export const CursorTopSelect: CoreEditorCommand = registerEditorCommand(new TopCommand({ + export const CursorTopSelect: CoreEditorCommand = registerEditorCommand(new TopCommand({ inSelectionMode: true, id: 'cursorTopSelect', precondition: undefined, @@ -1183,7 +1239,7 @@ export namespace CoreNavigationCommands { } })); - class BottomCommand extends CoreEditorCommand { + class BottomCommand extends CoreEditorCommand { private readonly _inSelectionMode: boolean; @@ -1192,7 +1248,7 @@ export namespace CoreNavigationCommands { this._inSelectionMode = opts.inSelectionMode; } - public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + public runCoreEditorCommand(viewModel: IViewModel, args: Partial): void { viewModel.model.pushStackElement(); viewModel.setCursorStates( args.source, @@ -1203,7 +1259,7 @@ export namespace CoreNavigationCommands { } } - export const CursorBottom: CoreEditorCommand = registerEditorCommand(new BottomCommand({ + export const CursorBottom: CoreEditorCommand = registerEditorCommand(new BottomCommand({ inSelectionMode: false, id: 'cursorBottom', precondition: undefined, @@ -1215,7 +1271,7 @@ export namespace CoreNavigationCommands { } })); - export const CursorBottomSelect: CoreEditorCommand = registerEditorCommand(new BottomCommand({ + export const CursorBottomSelect: CoreEditorCommand = registerEditorCommand(new BottomCommand({ inSelectionMode: true, id: 'cursorBottomSelect', precondition: undefined, @@ -1227,7 +1283,9 @@ export namespace CoreNavigationCommands { } })); - export class EditorScrollImpl extends CoreEditorCommand { + export type EditorScrollCommandOptions = EditorScroll_.RawArguments & BaseCommandOptions; + + export class EditorScrollImpl extends CoreEditorCommand { constructor() { super({ id: 'editorScroll', @@ -1236,7 +1294,7 @@ export namespace CoreNavigationCommands { }); } - public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + public runCoreEditorCommand(viewModel: IViewModel, args: Partial): void { const parsed = EditorScroll_.parse(args); if (!parsed) { // illegal arguments @@ -1307,7 +1365,7 @@ export namespace CoreNavigationCommands { export const EditorScroll: EditorScrollImpl = registerEditorCommand(new EditorScrollImpl()); - export const ScrollLineUp: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { + export const ScrollLineUp: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { constructor() { super({ id: 'scrollLineUp', @@ -1321,7 +1379,7 @@ export namespace CoreNavigationCommands { }); } - runCoreEditorCommand(viewModel: IViewModel, args: any): void { + runCoreEditorCommand(viewModel: IViewModel, args: Partial): void { EditorScroll._runEditorScroll(viewModel, args.source, { direction: EditorScroll_.Direction.Up, unit: EditorScroll_.Unit.WrappedLine, @@ -1332,7 +1390,7 @@ export namespace CoreNavigationCommands { } }); - export const ScrollPageUp: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { + export const ScrollPageUp: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { constructor() { super({ id: 'scrollPageUp', @@ -1347,7 +1405,7 @@ export namespace CoreNavigationCommands { }); } - runCoreEditorCommand(viewModel: IViewModel, args: any): void { + runCoreEditorCommand(viewModel: IViewModel, args: Partial): void { EditorScroll._runEditorScroll(viewModel, args.source, { direction: EditorScroll_.Direction.Up, unit: EditorScroll_.Unit.Page, @@ -1358,7 +1416,7 @@ export namespace CoreNavigationCommands { } }); - export const ScrollEditorTop: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { + export const ScrollEditorTop: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { constructor() { super({ id: 'scrollEditorTop', @@ -1370,7 +1428,7 @@ export namespace CoreNavigationCommands { }); } - runCoreEditorCommand(viewModel: IViewModel, args: any): void { + runCoreEditorCommand(viewModel: IViewModel, args: Partial): void { EditorScroll._runEditorScroll(viewModel, args.source, { direction: EditorScroll_.Direction.Up, unit: EditorScroll_.Unit.Editor, @@ -1381,7 +1439,7 @@ export namespace CoreNavigationCommands { } }); - export const ScrollLineDown: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { + export const ScrollLineDown: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { constructor() { super({ id: 'scrollLineDown', @@ -1395,7 +1453,7 @@ export namespace CoreNavigationCommands { }); } - runCoreEditorCommand(viewModel: IViewModel, args: any): void { + runCoreEditorCommand(viewModel: IViewModel, args: Partial): void { EditorScroll._runEditorScroll(viewModel, args.source, { direction: EditorScroll_.Direction.Down, unit: EditorScroll_.Unit.WrappedLine, @@ -1406,7 +1464,7 @@ export namespace CoreNavigationCommands { } }); - export const ScrollPageDown: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { + export const ScrollPageDown: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { constructor() { super({ id: 'scrollPageDown', @@ -1421,7 +1479,7 @@ export namespace CoreNavigationCommands { }); } - runCoreEditorCommand(viewModel: IViewModel, args: any): void { + runCoreEditorCommand(viewModel: IViewModel, args: Partial): void { EditorScroll._runEditorScroll(viewModel, args.source, { direction: EditorScroll_.Direction.Down, unit: EditorScroll_.Unit.Page, @@ -1432,7 +1490,7 @@ export namespace CoreNavigationCommands { } }); - export const ScrollEditorBottom: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { + export const ScrollEditorBottom: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { constructor() { super({ id: 'scrollEditorBottom', @@ -1444,7 +1502,7 @@ export namespace CoreNavigationCommands { }); } - runCoreEditorCommand(viewModel: IViewModel, args: any): void { + runCoreEditorCommand(viewModel: IViewModel, args: Partial): void { EditorScroll._runEditorScroll(viewModel, args.source, { direction: EditorScroll_.Direction.Down, unit: EditorScroll_.Unit.Editor, @@ -1455,7 +1513,7 @@ export namespace CoreNavigationCommands { } }); - class WordCommand extends CoreEditorCommand { + class WordCommand extends CoreEditorCommand { private readonly _inSelectionMode: boolean; @@ -1464,7 +1522,10 @@ export namespace CoreNavigationCommands { this._inSelectionMode = opts.inSelectionMode; } - public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + public runCoreEditorCommand(viewModel: IViewModel, args: Partial): void { + if (!args.position) { + return; + } viewModel.model.pushStackElement(); viewModel.setCursorStates( args.source, @@ -1473,23 +1534,25 @@ export namespace CoreNavigationCommands { CursorMoveCommands.word(viewModel, viewModel.getPrimaryCursorState(), this._inSelectionMode, args.position) ] ); - viewModel.revealPrimaryCursor(args.source, true); + if (args.revealType !== NavigationCommandRevealType.None) { + viewModel.revealPrimaryCursor(args.source, true, true); + } } } - export const WordSelect: CoreEditorCommand = registerEditorCommand(new WordCommand({ + export const WordSelect: CoreEditorCommand = registerEditorCommand(new WordCommand({ inSelectionMode: false, id: '_wordSelect', precondition: undefined })); - export const WordSelectDrag: CoreEditorCommand = registerEditorCommand(new WordCommand({ + export const WordSelectDrag: CoreEditorCommand = registerEditorCommand(new WordCommand({ inSelectionMode: true, id: '_wordSelectDrag', precondition: undefined })); - export const LastCursorWordSelect: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { + export const LastCursorWordSelect: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { constructor() { super({ id: 'lastCursorWordSelect', @@ -1497,7 +1560,10 @@ export namespace CoreNavigationCommands { }); } - public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + public runCoreEditorCommand(viewModel: IViewModel, args: Partial): void { + if (!args.position) { + return; + } const lastAddedCursorIndex = viewModel.getLastAddedCursorIndex(); const states = viewModel.getCursorStates(); @@ -1514,7 +1580,7 @@ export namespace CoreNavigationCommands { } }); - class LineCommand extends CoreEditorCommand { + class LineCommand extends CoreEditorCommand { private readonly _inSelectionMode: boolean; constructor(opts: ICommandOptions & { inSelectionMode: boolean }) { @@ -1522,7 +1588,10 @@ export namespace CoreNavigationCommands { this._inSelectionMode = opts.inSelectionMode; } - public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + public runCoreEditorCommand(viewModel: IViewModel, args: Partial): void { + if (!args.position) { + return; + } viewModel.model.pushStackElement(); viewModel.setCursorStates( args.source, @@ -1531,23 +1600,25 @@ export namespace CoreNavigationCommands { CursorMoveCommands.line(viewModel, viewModel.getPrimaryCursorState(), this._inSelectionMode, args.position, args.viewPosition) ] ); - viewModel.revealPrimaryCursor(args.source, false); + if (args.revealType !== NavigationCommandRevealType.None) { + viewModel.revealPrimaryCursor(args.source, false, true); + } } } - export const LineSelect: CoreEditorCommand = registerEditorCommand(new LineCommand({ + export const LineSelect: CoreEditorCommand = registerEditorCommand(new LineCommand({ inSelectionMode: false, id: '_lineSelect', precondition: undefined })); - export const LineSelectDrag: CoreEditorCommand = registerEditorCommand(new LineCommand({ + export const LineSelectDrag: CoreEditorCommand = registerEditorCommand(new LineCommand({ inSelectionMode: true, id: '_lineSelectDrag', precondition: undefined })); - class LastCursorLineCommand extends CoreEditorCommand { + class LastCursorLineCommand extends CoreEditorCommand { private readonly _inSelectionMode: boolean; constructor(opts: ICommandOptions & { inSelectionMode: boolean }) { @@ -1555,7 +1626,10 @@ export namespace CoreNavigationCommands { this._inSelectionMode = opts.inSelectionMode; } - public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + public runCoreEditorCommand(viewModel: IViewModel, args: Partial): void { + if (!args.position) { + return; + } const lastAddedCursorIndex = viewModel.getLastAddedCursorIndex(); const states = viewModel.getCursorStates(); @@ -1571,19 +1645,19 @@ export namespace CoreNavigationCommands { } } - export const LastCursorLineSelect: CoreEditorCommand = registerEditorCommand(new LastCursorLineCommand({ + export const LastCursorLineSelect: CoreEditorCommand = registerEditorCommand(new LastCursorLineCommand({ inSelectionMode: false, id: 'lastCursorLineSelect', precondition: undefined })); - export const LastCursorLineSelectDrag: CoreEditorCommand = registerEditorCommand(new LastCursorLineCommand({ + export const LastCursorLineSelectDrag: CoreEditorCommand = registerEditorCommand(new LastCursorLineCommand({ inSelectionMode: true, id: 'lastCursorLineSelectDrag', precondition: undefined })); - export const CancelSelection: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { + export const CancelSelection: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { constructor() { super({ id: 'cancelSelection', @@ -1597,7 +1671,7 @@ export namespace CoreNavigationCommands { }); } - public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + public runCoreEditorCommand(viewModel: IViewModel, args: Partial): void { viewModel.model.pushStackElement(); viewModel.setCursorStates( args.source, @@ -1610,7 +1684,7 @@ export namespace CoreNavigationCommands { } }); - export const RemoveSecondaryCursors: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { + export const RemoveSecondaryCursors: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { constructor() { super({ id: 'removeSecondaryCursors', @@ -1624,7 +1698,7 @@ export namespace CoreNavigationCommands { }); } - public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + public runCoreEditorCommand(viewModel: IViewModel, args: Partial): void { viewModel.model.pushStackElement(); viewModel.setCursorStates( args.source, @@ -1638,7 +1712,9 @@ export namespace CoreNavigationCommands { } }); - export const RevealLine: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { + export type RevealLineCommandOptions = RevealLine_.RawArguments & BaseCommandOptions; + + export const RevealLine: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { constructor() { super({ id: 'revealLine', @@ -1647,8 +1723,8 @@ export namespace CoreNavigationCommands { }); } - public runCoreEditorCommand(viewModel: IViewModel, args: any): void { - const revealLineArg = args; + public runCoreEditorCommand(viewModel: IViewModel, args: Partial): void { + const revealLineArg = args; const lineNumberArg = revealLineArg.lineNumber || 0; let lineNumber = typeof lineNumberArg === 'number' ? (lineNumberArg + 1) : (parseInt(lineNumberArg) + 1); if (lineNumber < 1) { @@ -1699,7 +1775,7 @@ export namespace CoreNavigationCommands { document.execCommand('selectAll'); } - public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void { + public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: unknown): void { const viewModel = editor._getViewModel(); if (!viewModel) { // the editor has no view => has no cursors @@ -1707,7 +1783,7 @@ export namespace CoreNavigationCommands { } this.runCoreEditorCommand(viewModel, args); } - public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + public runCoreEditorCommand(viewModel: IViewModel, args: unknown): void { viewModel.model.pushStackElement(); viewModel.setCursorStates( 'keyboard', @@ -1719,7 +1795,11 @@ export namespace CoreNavigationCommands { } }(); - export const SetSelection: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { + export interface SetSelectionCommandOptions extends BaseCommandOptions { + selection: ISelection; + } + + export const SetSelection: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { constructor() { super({ id: 'setSelection', @@ -1727,7 +1807,10 @@ export namespace CoreNavigationCommands { }); } - public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + public runCoreEditorCommand(viewModel: IViewModel, args: Partial): void { + if (!args.selection) { + return; + } viewModel.model.pushStackElement(); viewModel.setCursorStates( args.source, @@ -1768,7 +1851,7 @@ function registerCommand(command: T): T { export namespace CoreEditingCommands { export abstract class CoreEditingCommand extends EditorCommand { - public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void { + public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: unknown): void { const viewModel = editor._getViewModel(); if (!viewModel) { // the editor has no view => has no cursors @@ -1777,7 +1860,7 @@ export namespace CoreEditingCommands { this.runCoreEditingCommand(editor, viewModel, args || {}); } - public abstract runCoreEditingCommand(editor: ICodeEditor, viewModel: IViewModel, args: any): void; + public abstract runCoreEditingCommand(editor: ICodeEditor, viewModel: IViewModel, args: unknown): void; } export const LineBreakInsert: EditorCommand = registerEditorCommand(new class extends CoreEditingCommand { @@ -1794,7 +1877,7 @@ export namespace CoreEditingCommands { }); } - public runCoreEditingCommand(editor: ICodeEditor, viewModel: IViewModel, args: any): void { + public runCoreEditingCommand(editor: ICodeEditor, viewModel: IViewModel, args: unknown): void { editor.pushUndoStop(); editor.executeCommands(this.id, TypeOperations.lineBreakInsert(viewModel.cursorConfig, viewModel.model, viewModel.getCursorStates().map(s => s.modelState.selection))); } @@ -1816,7 +1899,7 @@ export namespace CoreEditingCommands { }); } - public runCoreEditingCommand(editor: ICodeEditor, viewModel: IViewModel, args: any): void { + public runCoreEditingCommand(editor: ICodeEditor, viewModel: IViewModel, args: unknown): void { editor.pushUndoStop(); editor.executeCommands(this.id, TypeOperations.outdent(viewModel.cursorConfig, viewModel.model, viewModel.getCursorStates().map(s => s.modelState.selection))); editor.pushUndoStop(); @@ -1839,7 +1922,7 @@ export namespace CoreEditingCommands { }); } - public runCoreEditingCommand(editor: ICodeEditor, viewModel: IViewModel, args: any): void { + public runCoreEditingCommand(editor: ICodeEditor, viewModel: IViewModel, args: unknown): void { editor.pushUndoStop(); editor.executeCommands(this.id, TypeOperations.tab(viewModel.cursorConfig, viewModel.model, viewModel.getCursorStates().map(s => s.modelState.selection))); editor.pushUndoStop(); @@ -1861,7 +1944,7 @@ export namespace CoreEditingCommands { }); } - public runCoreEditingCommand(editor: ICodeEditor, viewModel: IViewModel, args: any): void { + public runCoreEditingCommand(editor: ICodeEditor, viewModel: IViewModel, args: unknown): void { const [shouldPushStackElementBefore, commands] = DeleteOperations.deleteLeft(viewModel.getPrevEditOperationType(), viewModel.cursorConfig, viewModel.model, viewModel.getCursorStates().map(s => s.modelState.selection), viewModel.getCursorAutoClosedCharacters()); if (shouldPushStackElementBefore) { editor.pushUndoStop(); @@ -1885,7 +1968,7 @@ export namespace CoreEditingCommands { }); } - public runCoreEditingCommand(editor: ICodeEditor, viewModel: IViewModel, args: any): void { + public runCoreEditingCommand(editor: ICodeEditor, viewModel: IViewModel, args: unknown): void { const [shouldPushStackElementBefore, commands] = DeleteOperations.deleteRight(viewModel.getPrevEditOperationType(), viewModel.cursorConfig, viewModel.model, viewModel.getCursorStates().map(s => s.modelState.selection)); if (shouldPushStackElementBefore) { editor.pushUndoStop(); @@ -1902,7 +1985,7 @@ export namespace CoreEditingCommands { public runDOMCommand(): void { document.execCommand('undo'); } - public runEditorCommand(accessor: ServicesAccessor | null, editor: ICodeEditor, args: any): void | Promise { + public runEditorCommand(accessor: ServicesAccessor | null, editor: ICodeEditor, args: unknown): void | Promise { if (!editor.hasModel() || editor.getOption(EditorOption.readOnly) === true) { return; } @@ -1917,7 +2000,7 @@ export namespace CoreEditingCommands { public runDOMCommand(): void { document.execCommand('redo'); } - public runEditorCommand(accessor: ServicesAccessor | null, editor: ICodeEditor, args: any): void | Promise { + public runEditorCommand(accessor: ServicesAccessor | null, editor: ICodeEditor, args: unknown): void | Promise { if (!editor.hasModel() || editor.getOption(EditorOption.readOnly) === true) { return; } @@ -1942,7 +2025,7 @@ class EditorHandlerCommand extends Command { this._handlerId = handlerId; } - public runCommand(accessor: ServicesAccessor, args: any): void { + public runCommand(accessor: ServicesAccessor, args: unknown): void { const editor = accessor.get(ICodeEditorService).getFocusedCodeEditor(); if (!editor) { return; diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index d80aaf6042d..d768689d23a 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -391,6 +391,8 @@ export interface IMouseTargetOverviewRuler extends IBaseMouseTarget { } export interface IMouseTargetOutsideEditor extends IBaseMouseTarget { readonly type: MouseTargetType.OUTSIDE_EDITOR; + readonly outsidePosition: 'above' | 'below' | 'left' | 'right'; + readonly outsideDistance: number; } /** * Target hit with the mouse in the editor. diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index a05fae66338..6ee27a31b3f 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -245,6 +245,9 @@ export class View extends ViewEventHandler { const lastTextareaPosition = this._textAreaHandler.getLastRenderData(); return new PointerHandlerLastRenderData(lastViewCursorsRenderData, lastTextareaPosition); }, + renderNow: (): void => { + this.render(true, false); + }, shouldSuppressMouseDownOnViewZone: (viewZoneId: string) => { return this._viewZones.shouldSuppressMouseDownOnViewZone(viewZoneId); }, diff --git a/src/vs/editor/browser/view/viewController.ts b/src/vs/editor/browser/view/viewController.ts index 41d9b58a129..b6b8ebdd698 100644 --- a/src/vs/editor/browser/view/viewController.ts +++ b/src/vs/editor/browser/view/viewController.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { CoreNavigationCommands } from 'vs/editor/browser/coreCommands'; +import { CoreNavigationCommands, NavigationCommandRevealType } from 'vs/editor/browser/coreCommands'; import { IEditorMouseEvent, IPartialEditorMouseEvent } from 'vs/editor/browser/editorBrowser'; import { ViewUserInputEvents } from 'vs/editor/browser/view/viewUserInputEvents'; import { Position } from 'vs/editor/common/core/position'; @@ -21,6 +21,7 @@ export interface IMouseDispatchData { * Desired mouse column (e.g. when position.column gets clamped to text length -- clicking after text on a line). */ mouseColumn: number; + revealType: NavigationCommandRevealType; startedOnLineNumbers: boolean; inSelectionMode: boolean; @@ -138,15 +139,15 @@ export class ViewController { // If the dragging started on the gutter, then have operations work on the entire line if (this._hasMulticursorModifier(data)) { if (data.inSelectionMode) { - this._lastCursorLineSelect(data.position); + this._lastCursorLineSelect(data.position, data.revealType); } else { this._createCursor(data.position, true); } } else { if (data.inSelectionMode) { - this._lineSelectDrag(data.position); + this._lineSelectDrag(data.position, data.revealType); } else { - this._lineSelect(data.position); + this._lineSelect(data.position, data.revealType); } } } else if (data.mouseDownCount >= 4) { @@ -154,26 +155,26 @@ export class ViewController { } else if (data.mouseDownCount === 3) { if (this._hasMulticursorModifier(data)) { if (data.inSelectionMode) { - this._lastCursorLineSelectDrag(data.position); + this._lastCursorLineSelectDrag(data.position, data.revealType); } else { - this._lastCursorLineSelect(data.position); + this._lastCursorLineSelect(data.position, data.revealType); } } else { if (data.inSelectionMode) { - this._lineSelectDrag(data.position); + this._lineSelectDrag(data.position, data.revealType); } else { - this._lineSelect(data.position); + this._lineSelect(data.position, data.revealType); } } } else if (data.mouseDownCount === 2) { if (!data.onInjectedText) { if (this._hasMulticursorModifier(data)) { - this._lastCursorWordSelect(data.position); + this._lastCursorWordSelect(data.position, data.revealType); } else { if (data.inSelectionMode) { - this._wordSelectDrag(data.position); + this._wordSelectDrag(data.position, data.revealType); } else { - this._wordSelect(data.position); + this._wordSelect(data.position, data.revealType); } } } @@ -185,7 +186,7 @@ export class ViewController { } else { // Do multi-cursor operations only when purely alt is pressed if (data.inSelectionMode) { - this._lastCursorMoveToSelect(data.position); + this._lastCursorMoveToSelect(data.position, data.revealType); } else { this._createCursor(data.position, false); } @@ -199,31 +200,32 @@ export class ViewController { if (columnSelection) { this._columnSelect(data.position, data.mouseColumn, true); } else { - this._moveToSelect(data.position); + this._moveToSelect(data.position, data.revealType); } } } else { - this.moveTo(data.position); + this.moveTo(data.position, data.revealType); } } } } - private _usualArgs(viewPosition: Position) { + private _usualArgs(viewPosition: Position, revealType: NavigationCommandRevealType): CoreNavigationCommands.MoveCommandOptions { viewPosition = this._validateViewColumn(viewPosition); return { source: 'mouse', position: this._convertViewToModelPosition(viewPosition), - viewPosition: viewPosition + viewPosition, + revealType }; } - public moveTo(viewPosition: Position): void { - CoreNavigationCommands.MoveTo.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition)); + public moveTo(viewPosition: Position, revealType: NavigationCommandRevealType): void { + CoreNavigationCommands.MoveTo.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition, revealType)); } - private _moveToSelect(viewPosition: Position): void { - CoreNavigationCommands.MoveToSelect.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition)); + private _moveToSelect(viewPosition: Position, revealType: NavigationCommandRevealType): void { + CoreNavigationCommands.MoveToSelect.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition, revealType)); } private _columnSelect(viewPosition: Position, mouseColumn: number, doColumnSelect: boolean): void { @@ -247,36 +249,36 @@ export class ViewController { }); } - private _lastCursorMoveToSelect(viewPosition: Position): void { - CoreNavigationCommands.LastCursorMoveToSelect.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition)); + private _lastCursorMoveToSelect(viewPosition: Position, revealType: NavigationCommandRevealType): void { + CoreNavigationCommands.LastCursorMoveToSelect.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition, revealType)); } - private _wordSelect(viewPosition: Position): void { - CoreNavigationCommands.WordSelect.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition)); + private _wordSelect(viewPosition: Position, revealType: NavigationCommandRevealType): void { + CoreNavigationCommands.WordSelect.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition, revealType)); } - private _wordSelectDrag(viewPosition: Position): void { - CoreNavigationCommands.WordSelectDrag.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition)); + private _wordSelectDrag(viewPosition: Position, revealType: NavigationCommandRevealType): void { + CoreNavigationCommands.WordSelectDrag.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition, revealType)); } - private _lastCursorWordSelect(viewPosition: Position): void { - CoreNavigationCommands.LastCursorWordSelect.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition)); + private _lastCursorWordSelect(viewPosition: Position, revealType: NavigationCommandRevealType): void { + CoreNavigationCommands.LastCursorWordSelect.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition, revealType)); } - private _lineSelect(viewPosition: Position): void { - CoreNavigationCommands.LineSelect.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition)); + private _lineSelect(viewPosition: Position, revealType: NavigationCommandRevealType): void { + CoreNavigationCommands.LineSelect.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition, revealType)); } - private _lineSelectDrag(viewPosition: Position): void { - CoreNavigationCommands.LineSelectDrag.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition)); + private _lineSelectDrag(viewPosition: Position, revealType: NavigationCommandRevealType): void { + CoreNavigationCommands.LineSelectDrag.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition, revealType)); } - private _lastCursorLineSelect(viewPosition: Position): void { - CoreNavigationCommands.LastCursorLineSelect.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition)); + private _lastCursorLineSelect(viewPosition: Position, revealType: NavigationCommandRevealType): void { + CoreNavigationCommands.LastCursorLineSelect.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition, revealType)); } - private _lastCursorLineSelectDrag(viewPosition: Position): void { - CoreNavigationCommands.LastCursorLineSelectDrag.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition)); + private _lastCursorLineSelectDrag(viewPosition: Position, revealType: NavigationCommandRevealType): void { + CoreNavigationCommands.LastCursorLineSelectDrag.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition, revealType)); } private _selectAll(): void { diff --git a/src/vs/editor/browser/viewParts/lines/viewLines.ts b/src/vs/editor/browser/viewParts/lines/viewLines.ts index 805395c190b..8ca24cfe42b 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLines.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLines.ts @@ -102,7 +102,6 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost, private _typicalHalfwidthCharacterWidth: number; private _isViewportWrapping: boolean; private _revealHorizontalRightPadding: number; - private _horizontalScrollbarHeight: number; private _cursorSurroundingLines: number; private _cursorSurroundingLinesStyle: 'default' | 'all'; private _canUseLayerHinting: boolean; @@ -131,13 +130,11 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost, const options = this._context.configuration.options; const fontInfo = options.get(EditorOption.fontInfo); const wrappingInfo = options.get(EditorOption.wrappingInfo); - const layoutInfo = options.get(EditorOption.layoutInfo); this._lineHeight = options.get(EditorOption.lineHeight); this._typicalHalfwidthCharacterWidth = fontInfo.typicalHalfwidthCharacterWidth; this._isViewportWrapping = wrappingInfo.isViewportWrapping; this._revealHorizontalRightPadding = options.get(EditorOption.revealHorizontalRightPadding); - this._horizontalScrollbarHeight = layoutInfo.horizontalScrollbarHeight; this._cursorSurroundingLines = options.get(EditorOption.cursorSurroundingLines); this._cursorSurroundingLinesStyle = options.get(EditorOption.cursorSurroundingLinesStyle); this._canUseLayerHinting = !options.get(EditorOption.disableLayerHinting); @@ -194,13 +191,11 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost, const options = this._context.configuration.options; const fontInfo = options.get(EditorOption.fontInfo); const wrappingInfo = options.get(EditorOption.wrappingInfo); - const layoutInfo = options.get(EditorOption.layoutInfo); this._lineHeight = options.get(EditorOption.lineHeight); this._typicalHalfwidthCharacterWidth = fontInfo.typicalHalfwidthCharacterWidth; this._isViewportWrapping = wrappingInfo.isViewportWrapping; this._revealHorizontalRightPadding = options.get(EditorOption.revealHorizontalRightPadding); - this._horizontalScrollbarHeight = layoutInfo.horizontalScrollbarHeight; this._cursorSurroundingLines = options.get(EditorOption.cursorSurroundingLines); this._cursorSurroundingLinesStyle = options.get(EditorOption.cursorSurroundingLinesStyle); this._canUseLayerHinting = !options.get(EditorOption.disableLayerHinting); @@ -697,9 +692,11 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost, paddingTop = this._lineHeight; } } - if (verticalType === viewEvents.VerticalRevealType.Simple || verticalType === viewEvents.VerticalRevealType.Bottom) { - // Reveal one line more when the last line would be covered by the scrollbar - arrow down case or revealing a line explicitly at bottom - paddingBottom += (minimalReveal ? this._horizontalScrollbarHeight : this._lineHeight); + if (!minimalReveal) { + if (verticalType === viewEvents.VerticalRevealType.Simple || verticalType === viewEvents.VerticalRevealType.Bottom) { + // Reveal one line more when the last line would be covered by the scrollbar - arrow down case or revealing a line explicitly at bottom + paddingBottom += this._lineHeight; + } } boxStartY -= paddingTop; diff --git a/src/vs/editor/common/cursor/cursorMoveCommands.ts b/src/vs/editor/common/cursor/cursorMoveCommands.ts index 992c8c6ffd4..4c51a670f85 100644 --- a/src/vs/editor/common/cursor/cursorMoveCommands.ts +++ b/src/vs/editor/common/cursor/cursorMoveCommands.ts @@ -173,7 +173,7 @@ export class CursorMoveCommands { )); } - public static line(viewModel: IViewModel, cursor: CursorState, inSelectionMode: boolean, _position: IPosition, _viewPosition: IPosition): PartialCursorState { + public static line(viewModel: IViewModel, cursor: CursorState, inSelectionMode: boolean, _position: IPosition, _viewPosition: IPosition | undefined): PartialCursorState { const position = viewModel.model.validatePosition(_position); const viewPosition = ( _viewPosition @@ -251,7 +251,7 @@ export class CursorMoveCommands { )); } - public static moveTo(viewModel: IViewModel, cursor: CursorState, inSelectionMode: boolean, _position: IPosition, _viewPosition: IPosition): PartialCursorState { + public static moveTo(viewModel: IViewModel, cursor: CursorState, inSelectionMode: boolean, _position: IPosition, _viewPosition: IPosition | undefined): PartialCursorState { const position = viewModel.model.validatePosition(_position); const viewPosition = ( _viewPosition @@ -680,7 +680,7 @@ export namespace CursorMove { value?: number; } - export function parse(args: RawArguments): ParsedArguments | null { + export function parse(args: Partial): ParsedArguments | null { if (!args.to) { // illegal arguments return null; diff --git a/src/vs/editor/common/diff/standardLinesDiffComputer.ts b/src/vs/editor/common/diff/standardLinesDiffComputer.ts index 12de479e061..ce0b07c8a2e 100644 --- a/src/vs/editor/common/diff/standardLinesDiffComputer.ts +++ b/src/vs/editor/common/diff/standardLinesDiffComputer.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { assertFn, checkAdjacentItems } from 'vs/base/common/assert'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { SequenceFromIntArray, OffsetRange, SequenceDiff, ISequence } from 'vs/editor/common/diff/algorithms/diffAlgorithm'; @@ -91,7 +92,9 @@ export function lineRangeMappingFromRangeMappings(alignments: RangeMapping[]): L const changes: LineRangeMapping[] = []; for (const g of group( alignments, - (a1, a2) => a2.modifiedRange.startLineNumber - (a1.modifiedRange.endLineNumber - (a1.modifiedRange.endColumn > 1 ? 0 : 1)) <= 1 + (a1, a2) => + (a2.originalRange.startLineNumber - (a1.originalRange.endLineNumber - (a1.originalRange.endColumn > 1 ? 0 : 1)) <= 1) + || (a2.modifiedRange.startLineNumber - (a1.modifiedRange.endLineNumber - (a1.modifiedRange.endColumn > 1 ? 0 : 1)) <= 1) )) { const first = g[0]; const last = g[g.length - 1]; @@ -108,6 +111,17 @@ export function lineRangeMappingFromRangeMappings(alignments: RangeMapping[]): L g )); } + + assertFn(() => { + return checkAdjacentItems(changes, + (m1, m2) => m2.originalRange.startLineNumber - m1.originalRange.endLineNumberExclusive === m2.modifiedRange.startLineNumber - m1.modifiedRange.endLineNumberExclusive && + // There has to be an unchanged line in between (otherwise both diffs should have been joined) + m1.originalRange.endLineNumberExclusive < m2.originalRange.startLineNumber && + m1.modifiedRange.endLineNumberExclusive < m2.modifiedRange.startLineNumber, + ); + }); + + return changes; } diff --git a/src/vs/editor/contrib/codeAction/browser/codeAction.ts b/src/vs/editor/contrib/codeAction/browser/codeAction.ts index e79d1956f08..df00afaaacb 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeAction.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeAction.ts @@ -89,7 +89,9 @@ class ManagedCodeActionSet extends Disposable implements CodeActionSet { disposables: DisposableStore, ) { super(); + this._register(disposables); + this.allActions = [...actions].sort(ManagedCodeActionSet.codeActionsComparator); this.validActions = this.allActions.filter(({ action }) => !action.disabled); } @@ -134,7 +136,7 @@ export async function getCodeActions( } const filteredActions = (providedCodeActions?.actions || []).filter(action => action && filtersAction(filter, action)); - const documentation = getDocumentation(provider, filteredActions, filter.include); + const documentation = getDocumentationFromProvider(provider, filteredActions, filter.include); return { actions: filteredActions.map(action => new CodeActionItem(action, provider)), documentation @@ -158,7 +160,10 @@ export async function getCodeActions( try { const actions = await Promise.all(promises); const allActions = actions.map(x => x.actions).flat(); - const allDocumentation = coalesce(actions.map(x => x.documentation)); + const allDocumentation = [ + ...coalesce(actions.map(x => x.documentation)), + ...getAdditionalDocumentationForShowingActions(registry, model, trigger, allActions) + ]; return new ManagedCodeActionSet(allActions, allDocumentation, disposables); } finally { listener.dispose(); @@ -182,7 +187,22 @@ function getCodeActionProviders( }); } -function getDocumentation( +function* getAdditionalDocumentationForShowingActions( + registry: LanguageFeatureRegistry, + model: ITextModel, + trigger: CodeActionTrigger, + actionsToShow: readonly CodeActionItem[], +): Iterable { + if (model && actionsToShow.length) { + for (const provider of registry.all(model)) { + if (provider._getAdditionalMenuItems) { + yield* provider._getAdditionalMenuItems?.({ trigger: trigger.type, only: trigger.filter?.include?.value }, actionsToShow.map(item => item.action)); + } + } + } +} + +function getDocumentationFromProvider( provider: languages.CodeActionProvider, providedCodeActions: readonly languages.CodeAction[], only?: CodeActionKind diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts b/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts index d3b609d20cb..8e002a60009 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts @@ -11,7 +11,7 @@ import { Lazy } from 'vs/base/common/lazy'; import { Disposable } from 'vs/base/common/lifecycle'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { EditorAction, EditorCommand, registerEditorCommand, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { EditorAction, EditorCommand, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { IPosition } from 'vs/editor/common/core/position'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; @@ -19,20 +19,21 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { CodeActionTriggerType } from 'vs/editor/common/languages'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { codeActionCommandId, CodeActionItem, CodeActionSet, fixAllCommandId, organizeImportsCommandId, refactorCommandId, refactorPreviewCommandId, sourceActionCommandId } from 'vs/editor/contrib/codeAction/browser/codeAction'; +import { acceptSelectedCodeActionCommand, CodeActionWidget, Context, previewSelectedCodeActionCommand } from 'vs/editor/contrib/codeAction/browser/codeActionWidget'; import { CodeActionUi } from 'vs/editor/contrib/codeAction/browser/codeActionUi'; import { MessageController } from 'vs/editor/contrib/message/browser/messageController'; import * as nls from 'vs/nls'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IMarkerService } from 'vs/platform/markers/common/markers'; -import { IEditorProgressService } from 'vs/platform/progress/common/progress'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IEditorProgressService } from 'vs/platform/progress/common/progress'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { CodeActionModel, CodeActionsState, SUPPORTED_CODE_ACTIONS } from './codeActionModel'; import { CodeActionAutoApply, CodeActionCommandArgs, CodeActionFilter, CodeActionKind, CodeActionTrigger, CodeActionTriggerSource } from './types'; -import { acceptSelectedCodeActionCommand, Context, previewSelectedCodeActionCommand } from 'vs/editor/contrib/codeAction/browser/codeActionMenu'; function contextKeyForSupportedActions(kind: CodeActionKind) { return ContextKeyExpr.regex( @@ -87,12 +88,12 @@ const argsSchema: IJSONSchema = { } }; -export class QuickFixController extends Disposable implements IEditorContribution { +export class CodeActionController extends Disposable implements IEditorContribution { - public static readonly ID = 'editor.contrib.quickFixController'; + public static readonly ID = 'editor.contrib.codeActionController'; - public static get(editor: ICodeEditor): QuickFixController | null { - return editor.getContribution(QuickFixController.ID); + public static get(editor: ICodeEditor): CodeActionController | null { + return editor.getContribution(CodeActionController.ID); } private readonly _editor: ICodeEditor; @@ -113,9 +114,8 @@ export class QuickFixController extends Disposable implements IEditorContributio this._model = this._register(new CodeActionModel(this._editor, languageFeaturesService.codeActionProvider, markerService, contextKeyService, progressService)); this._register(this._model.onDidChangeState(newState => this.update(newState))); - this._ui = new Lazy(() => - this._register(new CodeActionUi(editor, QuickFixAction.Id, AutoFixAction.Id, { + this._register(_instantiationService.createInstance(CodeActionUi, editor, QuickFixAction.Id, AutoFixAction.Id, { applyCodeAction: async (action, retrigger, preview) => { try { await this._applyCodeAction(action, preview); @@ -125,7 +125,7 @@ export class QuickFixController extends Disposable implements IEditorContributio } } } - }, this._instantiationService)) + })) ); } @@ -133,31 +133,6 @@ export class QuickFixController extends Disposable implements IEditorContributio this._ui.getValue().update(newState); } - public hideCodeActionMenu() { - if (this._ui.hasValue()) { - this._ui.getValue().hideCodeActionWidget(); - } - } - - public navigateCodeActionList(navUp: Boolean) { - if (this._ui.hasValue()) { - this._ui.getValue().navigateList(navUp); - } - } - - public selectedOption() { - if (this._ui.hasValue()) { - this._ui.getValue().onEnter(); - } - } - - public selectedOptionWithPreview() { - if (this._ui.hasValue()) { - this._ui.getValue().onPreviewEnter(); - } - - } - public showCodeActions(trigger: CodeActionTrigger, actions: CodeActionSet, at: IAnchor | IPosition) { return this._ui.getValue().showCodeActionList(trigger, actions, at, { includeDisabledActions: false, fromLightbulb: false }); } @@ -168,7 +143,6 @@ export class QuickFixController extends Disposable implements IEditorContributio filter?: CodeActionFilter, autoApply?: CodeActionAutoApply, preview?: boolean, - ): void { if (!this._editor.hasModel()) { return; @@ -273,7 +247,7 @@ function triggerCodeActionsForEditorSelection( triggerAction: CodeActionTriggerSource = CodeActionTriggerSource.Default ): void { if (editor.hasModel()) { - const controller = QuickFixController.get(editor); + const controller = CodeActionController.get(editor); controller?.manualTriggerAtCurrentPosition(notAvailableMessage, triggerAction, filter, autoApply, preview); } } @@ -517,72 +491,116 @@ export class AutoFixAction extends EditorAction { } } -const CodeActionContribution = EditorCommand.bindToContribution(QuickFixController.get); - const weight = KeybindingWeight.EditorContrib + 90; -registerEditorCommand(new CodeActionContribution({ - id: 'hideCodeActionWidget', - precondition: Context.Visible, - handler(x) { - x.hideCodeActionMenu(); - }, - kbOpts: { - weight: weight, - primary: KeyCode.Escape, - secondary: [KeyMod.Shift | KeyCode.Escape] +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'hideCodeActionWidget', + title: { + value: nls.localize('hideCodeActionWidget.title', "Hide code action widget"), + original: 'Hide code action widget' + }, + precondition: Context.Visible, + keybinding: { + weight: weight, + primary: KeyCode.Escape, + secondary: [KeyMod.Shift | KeyCode.Escape] + }, + }); } -})); -registerEditorCommand(new CodeActionContribution({ - id: 'selectPrevCodeAction', - precondition: Context.Visible, - handler(x) { - x.navigateCodeActionList(true); - }, - kbOpts: { - weight: weight + 100000, - primary: KeyCode.UpArrow, - secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow], - mac: { primary: KeyCode.UpArrow, secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow, KeyMod.WinCtrl | KeyCode.KeyP] }, + run(): void { + CodeActionWidget.INSTANCE?.hide(); } -})); +}); -registerEditorCommand(new CodeActionContribution({ - id: 'selectNextCodeAction', - precondition: Context.Visible, - handler(x) { - x.navigateCodeActionList(false); - }, - kbOpts: { - weight: weight + 100000, - primary: KeyCode.DownArrow, - secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow], - mac: { primary: KeyCode.DownArrow, secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow, KeyMod.WinCtrl | KeyCode.KeyN] } +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'selectPrevCodeAction', + title: { + value: nls.localize('selectPrevCodeAction.title', "Select previous code action"), + original: 'Select previous code action' + }, + precondition: Context.Visible, + keybinding: { + weight: weight + 100000, + primary: KeyCode.UpArrow, + secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow], + mac: { primary: KeyCode.UpArrow, secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow, KeyMod.WinCtrl | KeyCode.KeyP] }, + } + }); } -})); -registerEditorCommand(new CodeActionContribution({ - id: acceptSelectedCodeActionCommand, - precondition: Context.Visible, - handler(x) { - x.selectedOption(); - }, - kbOpts: { - weight: weight + 100000, - primary: KeyCode.Enter, - secondary: [KeyMod.CtrlCmd | KeyCode.Period], + run(): void { + CodeActionWidget.INSTANCE?.focusPrevious(); } -})); +}); -registerEditorCommand(new CodeActionContribution({ - id: previewSelectedCodeActionCommand, - precondition: Context.Visible, - handler(x) { - x.selectedOptionWithPreview(); - }, - kbOpts: { - weight: weight + 100000, - primary: KeyMod.CtrlCmd | KeyCode.Enter, +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'selectNextCodeAction', + title: { + value: nls.localize('selectNextCodeAction.title', "Select next code action"), + original: 'Select next code action' + }, + precondition: Context.Visible, + keybinding: { + weight: weight + 100000, + primary: KeyCode.DownArrow, + secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow], + mac: { primary: KeyCode.DownArrow, secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow, KeyMod.WinCtrl | KeyCode.KeyN] } + } + }); } -})); + + run(): void { + CodeActionWidget.INSTANCE?.focusNext(); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: acceptSelectedCodeActionCommand, + title: { + value: nls.localize('acceptSelected.title', "Accept selected code action"), + original: 'Accept selected code action' + }, + precondition: Context.Visible, + keybinding: { + weight: weight + 100000, + primary: KeyCode.Enter, + secondary: [KeyMod.CtrlCmd | KeyCode.Period], + } + }); + } + + run(): void { + CodeActionWidget.INSTANCE?.acceptSelected(); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: previewSelectedCodeActionCommand, + title: { + value: nls.localize('previewSelected.title', "Preview selected code action"), + original: 'Preview selected code action' + }, + precondition: Context.Visible, + keybinding: { + weight: weight + 100000, + primary: KeyMod.CtrlCmd | KeyCode.Enter, + } + }); + } + + run(): void { + CodeActionWidget.INSTANCE?.acceptSelected({ preview: true }); + } +}); + diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionContributions.ts b/src/vs/editor/contrib/codeAction/browser/codeActionContributions.ts index 59cd3327bea..75c76189bc1 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionContributions.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionContributions.ts @@ -4,10 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { registerEditorAction, registerEditorCommand, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { AutoFixAction, CodeActionCommand, FixAllAction, OrganizeImportsAction, QuickFixAction, QuickFixController, RefactorAction, RefactorPreview, SourceAction } from 'vs/editor/contrib/codeAction/browser/codeActionCommands'; -import 'vs/editor/contrib/codeAction/browser/codeActionWidgetContribution'; +import { editorConfigurationBaseNode } from 'vs/editor/common/config/editorConfigurationSchema'; +import { AutoFixAction, CodeActionCommand, CodeActionController, FixAllAction, OrganizeImportsAction, QuickFixAction, RefactorAction, RefactorPreview, SourceAction } from 'vs/editor/contrib/codeAction/browser/codeActionCommands'; +import * as nls from 'vs/nls'; +import { ConfigurationScope, Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { Registry } from 'vs/platform/registry/common/platform'; -registerEditorContribution(QuickFixController.ID, QuickFixController); +registerEditorContribution(CodeActionController.ID, CodeActionController); registerEditorAction(QuickFixAction); registerEditorAction(RefactorAction); registerEditorAction(RefactorPreview); @@ -16,3 +19,15 @@ registerEditorAction(OrganizeImportsAction); registerEditorAction(AutoFixAction); registerEditorAction(FixAllAction); registerEditorCommand(new CodeActionCommand()); + +Registry.as(Extensions.Configuration).registerConfiguration({ + ...editorConfigurationBaseNode, + properties: { + 'editor.codeActionWidget.showHeaders': { + type: 'boolean', + scope: ConfigurationScope.LANGUAGE_OVERRIDABLE, + description: nls.localize('showCodeActionHeaders', "Enable/disable showing group headers in the code action menu."), + default: true, + }, + } +}); diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionModel.ts b/src/vs/editor/contrib/codeAction/browser/codeActionModel.ts index d2eeb268d1d..132de32c9ff 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionModel.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionModel.ts @@ -69,21 +69,6 @@ class CodeActionOracle extends Disposable { }, this._delay); } - private _getRangeOfMarker(selection: Selection): Range | undefined { - const model = this._editor.getModel(); - if (!model) { - return undefined; - } - for (const marker of this._markerService.read({ resource: model.uri })) { - const markerRange = model.validateRange(marker); - if (Range.intersectRanges(markerRange, selection)) { - return Range.lift(markerRange); - } - } - - return undefined; - } - private _getRangeOfSelectionUnlessWhitespaceEnclosed(trigger: CodeActionTrigger): Selection | undefined { if (!this._editor.hasModel()) { return undefined; @@ -124,13 +109,10 @@ class CodeActionOracle extends Disposable { return undefined; } - const markerRange = this._getRangeOfMarker(selection); - const position = markerRange ? markerRange.getStartPosition() : selection.getStartPosition(); - const e: TriggeredCodeAction = { trigger, selection, - position + position: selection.getStartPosition(), }; this._signalChange(e); return e; diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionUi.ts b/src/vs/editor/contrib/codeAction/browser/codeActionUi.ts index d5af03fc557..13e0d5fe187 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionUi.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionUi.ts @@ -14,18 +14,16 @@ import { ScrollType } from 'vs/editor/common/editorCommon'; import { CodeActionTriggerType } from 'vs/editor/common/languages'; import { CodeActionItem, CodeActionSet } from 'vs/editor/contrib/codeAction/browser/codeAction'; import { MessageController } from 'vs/editor/contrib/message/browser/messageController'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { CodeActionMenu, CodeActionShowOptions } from './codeActionMenu'; import { CodeActionsState } from './codeActionModel'; +import { CodeActionShowOptions, CodeActionWidget } from './codeActionWidget'; import { LightBulbWidget } from './lightBulbWidget'; import { CodeActionAutoApply, CodeActionTrigger } from './types'; export class CodeActionUi extends Disposable { - - private readonly _codeActionWidget: Lazy; private readonly _lightBulbWidget: Lazy; private readonly _activeCodeActions = this._register(new MutableDisposable()); - private previewOn: boolean = false; #disposed = false; @@ -36,28 +34,19 @@ export class CodeActionUi extends Disposable { private readonly delegate: { applyCodeAction: (action: CodeActionItem, regtriggerAfterApply: boolean, preview: boolean) => Promise; }, - @IInstantiationService instantiationService: IInstantiationService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IConfigurationService private readonly _configurationService: IConfigurationService, ) { super(); - this._codeActionWidget = new Lazy(() => { - return this._register(instantiationService.createInstance(CodeActionMenu, this._editor, { - onSelectCodeAction: async (action, trigger) => { - if (this.previewOn) { - this.delegate.applyCodeAction(action, /* retrigger */ true, Boolean(this.previewOn)); - } else { - this.delegate.applyCodeAction(action, /* retrigger */ true, Boolean(trigger.preview)); - } - this.previewOn = false; - } - })); - }); this._lightBulbWidget = new Lazy(() => { - const widget = this._register(instantiationService.createInstance(LightBulbWidget, this._editor, quickFixActionId, preferredFixActionId)); - this._register(widget.onClick(e => this.showCodeActionList(e.trigger, e.actions, e, { includeDisabledActions: false, fromLightbulb: true }))); + const widget = this._register(_instantiationService.createInstance(LightBulbWidget, this._editor, quickFixActionId, preferredFixActionId)); + this._register(widget.onClick(e => this.showCodeActionList(e.trigger, e.actions, e, { includeDisabledActions: false, fromLightbulb: true, showHeaders: this.shouldShowHeaders() }))); return widget; }); + + this._register(this._editor.onDidLayoutChange(() => CodeActionWidget.INSTANCE?.hide())); } override dispose() { @@ -65,27 +54,6 @@ export class CodeActionUi extends Disposable { super.dispose(); } - public hideCodeActionWidget() { - this._codeActionWidget.rawValue?.hide(); - } - - public onEnter() { - this._codeActionWidget.rawValue?.acceptSelected(); - } - - public onPreviewEnter() { - this.previewOn = true; - this.onEnter(); - } - - public navigateList(navUp: Boolean) { - if (navUp) { - this._codeActionWidget.rawValue?.focusPrevious(); - } else { - this._codeActionWidget.rawValue?.focusNext(); - } - } - public async update(newState: CodeActionsState.State): Promise { if (newState.type !== CodeActionsState.Type.Triggered) { this._lightBulbWidget.rawValue?.hide(); @@ -143,10 +111,10 @@ export class CodeActionUi extends Disposable { } this._activeCodeActions.value = actions; - this._codeActionWidget.getValue().show(newState.trigger, actions, this.toCoords(newState.position), { includeDisabledActions, fromLightbulb: false }); + this.showCodeActionList(newState.trigger, actions, this.toCoords(newState.position), { includeDisabledActions, fromLightbulb: false, showHeaders: this.shouldShowHeaders() }); } else { // auto magically triggered - if (this._codeActionWidget.getValue().isVisible) { + if (CodeActionWidget.INSTANCE?.isVisible) { // TODO: Figure out if we should update the showing menu? actions.dispose(); } else { @@ -184,8 +152,21 @@ export class CodeActionUi extends Disposable { } public async showCodeActionList(trigger: CodeActionTrigger, actions: CodeActionSet, at: IAnchor | IPosition, options: CodeActionShowOptions): Promise { + const editorDom = this._editor.getDomNode(); + if (!editorDom) { + return; + } + const anchor = Position.isIPosition(at) ? this.toCoords(at) : at; - this._codeActionWidget.getValue().show(trigger, actions, anchor, options); + + CodeActionWidget.getOrCreateInstance(this._instantiationService).show(trigger, actions, anchor, editorDom, { ...options, showHeaders: this.shouldShowHeaders() }, { + onSelectCodeAction: async (action, trigger, options) => { + this.delegate.applyCodeAction(action, /* retrigger */ true, Boolean(options.preview || trigger.preview)); + }, + onHide: () => { + this._editor?.focus(); + }, + }); } private toCoords(position: IPosition): IAnchor { @@ -204,4 +185,9 @@ export class CodeActionUi extends Disposable { return { x, y }; } + + private shouldShowHeaders(): boolean { + const model = this._editor?.getModel(); + return this._configurationService.getValue('editor.codeActionWidget.showHeaders', { resource: model?.uri }); + } } diff --git a/src/vs/editor/contrib/codeAction/browser/media/action.css b/src/vs/editor/contrib/codeAction/browser/codeActionWidget.css similarity index 100% rename from src/vs/editor/contrib/codeAction/browser/media/action.css rename to src/vs/editor/contrib/codeAction/browser/codeActionWidget.css diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionMenu.ts b/src/vs/editor/contrib/codeAction/browser/codeActionWidget.ts similarity index 83% rename from src/vs/editor/contrib/codeAction/browser/codeActionMenu.ts rename to src/vs/editor/contrib/codeAction/browser/codeActionWidget.ts index 5f907a26740..7c3180aa563 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionMenu.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionWidget.ts @@ -14,19 +14,15 @@ import { IAction } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { OS } from 'vs/base/common/platform'; -import 'vs/css!./media/action'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IEditorContribution } from 'vs/editor/common/editorCommon'; -import { Command } from 'vs/editor/common/languages'; -import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import 'vs/css!./codeActionWidget'; import { CodeActionItem, CodeActionSet } from 'vs/editor/contrib/codeAction/browser/codeAction'; import { CodeActionKind, CodeActionTrigger, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/browser/types'; import 'vs/editor/contrib/symbolIcons/browser/symbolIcons'; // The codicon symbol colors are defined here and must be loaded to get colors import { localize } from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { CodeActionKeybindingResolver } from './codeActionKeybindingResolver'; @@ -39,12 +35,14 @@ export const acceptSelectedCodeActionCommand = 'acceptSelectedCodeAction'; export const previewSelectedCodeActionCommand = 'previewSelectedCodeAction'; interface CodeActionWidgetDelegate { - onSelectCodeAction: (action: CodeActionItem, trigger: CodeActionTrigger) => Promise; + onSelectCodeAction(action: CodeActionItem, trigger: CodeActionTrigger, options: { readonly preview: boolean }): Promise; + onHide(cancelled: boolean): void; } export interface CodeActionShowOptions { readonly includeDisabledActions: boolean; readonly fromLightbulb?: boolean; + readonly showHeaders?: boolean; } enum CodeActionListItemKind { @@ -179,6 +177,7 @@ class HeaderRenderer implements IListRenderer void, + private readonly onDidSelect: (action: CodeActionItem, options: { readonly preview: boolean }) => void, @IKeybindingService keybindingService: IKeybindingService, ) { super(); @@ -274,7 +273,7 @@ class CodeActionList extends Disposable { this.list.focusNext(1, true, undefined, element => element.kind === CodeActionListItemKind.CodeAction && !element.action.action.disabled); } - public acceptSelected() { + public acceptSelected(options?: { readonly preview?: boolean }) { const focused = this.list.getFocus(); if (focused.length === 0) { return; @@ -286,7 +285,8 @@ class CodeActionList extends Disposable { return; } - this.list.setSelection([focusIndex]); + const event = new UIEvent(options?.preview ? previewSelectedEventType : 'acceptSelectedCodeAction'); + this.list.setSelection([focusIndex], event); } private onListSelection(e: IListEvent): void { @@ -296,7 +296,7 @@ class CodeActionList extends Disposable { const element = e.elements[0]; if (element.kind === CodeActionListItemKind.CodeAction && !element.action.action.disabled) { - this.onDidSelect(element.action); + this.onDidSelect(element.action, { preview: e.browserEvent?.type === previewSelectedEventType }); } else { this.list.setSelection([]); } @@ -347,12 +347,17 @@ class CodeActionList extends Disposable { // TODO: Take a look at user storage for this so it is preserved across windows and on reload. let showDisabled = false; -export class CodeActionMenu extends Disposable implements IEditorContribution { +export class CodeActionWidget extends Disposable { - public static readonly ID: string = 'editor.contrib.codeActionMenu'; + private static _instance?: CodeActionWidget; - public static get(editor: ICodeEditor): CodeActionMenu | null { - return editor.getContribution(CodeActionMenu.ID); + public static get INSTANCE(): CodeActionWidget | undefined { return this._instance; } + + public static getOrCreateInstance(instantiationService: IInstantiationService): CodeActionWidget { + if (!this._instance) { + this._instance = instantiationService.createInstance(CodeActionWidget); + } + return this._instance; } private readonly codeActionList = this._register(new MutableDisposable()); @@ -361,20 +366,18 @@ export class CodeActionMenu extends Disposable implements IEditorContribution { readonly options: CodeActionShowOptions; readonly trigger: CodeActionTrigger; readonly anchor: IAnchor; + readonly container: HTMLElement | undefined; readonly codeActions: CodeActionSet; + readonly delegate: CodeActionWidgetDelegate; }; private readonly _ctxMenuWidgetVisible: IContextKey; constructor( - private readonly _editor: ICodeEditor, - private readonly _delegate: CodeActionWidgetDelegate, @ICommandService private readonly _commandService: ICommandService, - @IConfigurationService private readonly _configurationService: IConfigurationService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IContextViewService private readonly _contextViewService: IContextViewService, @IKeybindingService private readonly _keybindingService: IKeybindingService, - @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @ITelemetryService private readonly _telemetryService: ITelemetryService, ) { super(); @@ -386,42 +389,33 @@ export class CodeActionMenu extends Disposable implements IEditorContribution { return !!this.currentShowingContext; } - public async show(trigger: CodeActionTrigger, codeActions: CodeActionSet, anchor: IAnchor, options: CodeActionShowOptions): Promise { + public async show(trigger: CodeActionTrigger, codeActions: CodeActionSet, anchor: IAnchor, container: HTMLElement | undefined, options: CodeActionShowOptions, delegate: CodeActionWidgetDelegate): Promise { this.currentShowingContext = undefined; - if (!this._editor.hasModel() || !this._editor.getDomNode()) { - return; - } const actionsToShow = options.includeDisabledActions && (showDisabled || codeActions.validActions.length === 0) ? codeActions.allActions : codeActions.validActions; if (!actionsToShow.length) { return; } - this.currentShowingContext = { trigger, codeActions, anchor, options }; + this.currentShowingContext = { trigger, codeActions, anchor, container, delegate, options }; this._contextViewService.showContextView({ getAnchor: () => anchor, - render: (container: HTMLElement) => this.renderWidget(container, trigger, codeActions, options, actionsToShow), - onHide: (didCancel: boolean) => this.onWidgetClosed(trigger, options, codeActions, didCancel), - }, this._editor.getDomNode()!, false); + render: (container: HTMLElement) => this.renderWidget(container, trigger, codeActions, options, actionsToShow, delegate), + onHide: (didCancel: boolean) => this.onWidgetClosed(trigger, options, codeActions, didCancel, delegate), + }, container, false); } - /** - * Focuses on the previous item in the list using the list widget. - */ public focusPrevious() { this.codeActionList.value?.focusPrevious(); } - /** - * Focuses on the next item in the list using the list widget. - */ public focusNext() { this.codeActionList.value?.focusNext(); } - public acceptSelected() { - this.codeActionList.value?.acceptSelected(); + public acceptSelected(options?: { readonly preview?: boolean }) { + this.codeActionList.value?.acceptSelected(options); } public hide() { @@ -430,12 +424,7 @@ export class CodeActionMenu extends Disposable implements IEditorContribution { this._contextViewService.hideContextView(); } - private shouldShowHeaders(): boolean { - const model = this._editor.getModel(); - return this._configurationService.getValue('editor.codeActionWidget.showHeaders', { resource: model?.uri }); - } - - private renderWidget(element: HTMLElement, trigger: CodeActionTrigger, codeActions: CodeActionSet, options: CodeActionShowOptions, showingCodeActions: readonly CodeActionItem[]): IDisposable { + private renderWidget(element: HTMLElement, trigger: CodeActionTrigger, codeActions: CodeActionSet, options: CodeActionShowOptions, showingCodeActions: readonly CodeActionItem[], delegate: CodeActionWidgetDelegate): IDisposable { const renderDisposables = new DisposableStore(); const widget = document.createElement('div'); @@ -444,10 +433,10 @@ export class CodeActionMenu extends Disposable implements IEditorContribution { this.codeActionList.value = new CodeActionList( showingCodeActions, - this.shouldShowHeaders(), - action => { + options.showHeaders ?? true, + (action, options) => { this.hide(); - this._delegate.onSelectCodeAction(action, trigger); + delegate.onSelectCodeAction(action, trigger, options); }, this._keybindingService); @@ -485,7 +474,7 @@ export class CodeActionMenu extends Disposable implements IEditorContribution { // Action bar let actionBarWidth = 0; if (!options.fromLightbulb) { - const actionBar = this.createActionBar(trigger, showingCodeActions, codeActions, options); + const actionBar = this.createActionBar(codeActions, options); if (actionBar) { widget.appendChild(actionBar.getContainer().parentElement!); renderDisposables.add(actionBar); @@ -496,8 +485,6 @@ export class CodeActionMenu extends Disposable implements IEditorContribution { const width = this.codeActionList.value.layout(actionBarWidth); widget.style.width = `${width}px`; - renderDisposables.add(this._editor.onDidLayoutChange(() => this.hide())); - const focusTracker = renderDisposables.add(dom.trackFocus(element)); renderDisposables.add(focusTracker.onDidBlur(() => this.hide())); @@ -510,18 +497,18 @@ export class CodeActionMenu extends Disposable implements IEditorContribution { * Toggles whether the disabled actions in the code action widget are visible or not. */ private toggleShowDisabled(newShowDisabled: boolean): void { - const previouslyShowingActions = this.currentShowingContext; + const previousCtx = this.currentShowingContext; this.hide(); showDisabled = newShowDisabled; - if (previouslyShowingActions) { - this.show(previouslyShowingActions.trigger, previouslyShowingActions.codeActions, previouslyShowingActions.anchor, previouslyShowingActions.options); + if (previousCtx) { + this.show(previousCtx.trigger, previousCtx.codeActions, previousCtx.anchor, previousCtx.container, previousCtx.options, previousCtx.delegate); } } - private onWidgetClosed(trigger: CodeActionTrigger, options: CodeActionShowOptions, codeActions: CodeActionSet, didCancel: boolean): void { + private onWidgetClosed(trigger: CodeActionTrigger, options: CodeActionShowOptions, codeActions: CodeActionSet, cancelled: boolean, delegate: CodeActionWidgetDelegate): void { type ApplyCodeActionEvent = { codeActionFrom: CodeActionTriggerSource; validCodeActions: number; @@ -539,15 +526,15 @@ export class CodeActionMenu extends Disposable implements IEditorContribution { this._telemetryService.publicLog2('codeAction.applyCodeAction', { codeActionFrom: options.fromLightbulb ? CodeActionTriggerSource.Lightbulb : trigger.triggerAction, validCodeActions: codeActions.validActions.length, - cancelled: didCancel, + cancelled: cancelled, }); this.currentShowingContext = undefined; - this._editor.focus(); + delegate.onHide(cancelled); } - private createActionBar(trigger: CodeActionTrigger, inputCodeActions: readonly CodeActionItem[], codeActions: CodeActionSet, options: CodeActionShowOptions): ActionBar | undefined { - const actions = this.getActionBarActions(trigger, inputCodeActions, codeActions, options); + private createActionBar(codeActions: CodeActionSet, options: CodeActionShowOptions): ActionBar | undefined { + const actions = this.getActionBarActions(codeActions, options); if (!actions.length) { return undefined; } @@ -558,8 +545,15 @@ export class CodeActionMenu extends Disposable implements IEditorContribution { return actionBar; } - private getActionBarActions(trigger: CodeActionTrigger, inputCodeActions: readonly CodeActionItem[], codeActions: CodeActionSet, options: CodeActionShowOptions): IAction[] { - const actions = this.getDocumentationActions(trigger, inputCodeActions, codeActions.documentation); + private getActionBarActions(codeActions: CodeActionSet, options: CodeActionShowOptions): IAction[] { + const actions = codeActions.documentation.map((command): IAction => ({ + id: command.id, + label: command.title, + tooltip: command.tooltip ?? '', + class: undefined, + enabled: true, + run: () => this._commandService.executeCommand(command.id, ...(command.arguments ?? [])), + })); if (options.includeDisabledActions && codeActions.validActions.length > 0 && codeActions.allActions.length !== codeActions.validActions.length) { actions.push(showDisabled ? { @@ -578,32 +572,7 @@ export class CodeActionMenu extends Disposable implements IEditorContribution { run: () => this.toggleShowDisabled(true) }); } + return actions; } - - private getDocumentationActions( - trigger: CodeActionTrigger, - actionsToShow: readonly CodeActionItem[], - documentation: readonly Command[], - ): IAction[] { - const allDocumentation: Command[] = [...documentation]; - - const model = this._editor.getModel(); - if (model && actionsToShow.length) { - for (const provider of this._languageFeaturesService.codeActionProvider.all(model)) { - if (provider._getAdditionalMenuItems) { - allDocumentation.push(...provider._getAdditionalMenuItems({ trigger: trigger.type, only: trigger.filter?.include?.value }, actionsToShow.map(item => item.action))); - } - } - } - - return allDocumentation.map((command): IAction => ({ - id: command.id, - label: command.title, - tooltip: command.tooltip ?? '', - class: undefined, - enabled: true, - run: () => this._commandService.executeCommand(command.id, ...(command.arguments ?? [])), - })); - } } diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionWidgetContribution.ts b/src/vs/editor/contrib/codeAction/browser/codeActionWidgetContribution.ts deleted file mode 100644 index f8c7d5eb2c0..00000000000 --- a/src/vs/editor/contrib/codeAction/browser/codeActionWidgetContribution.ts +++ /dev/null @@ -1,21 +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 { editorConfigurationBaseNode } from 'vs/editor/common/config/editorConfigurationSchema'; -import * as nls from 'vs/nls'; -import { ConfigurationScope, Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; -import { Registry } from 'vs/platform/registry/common/platform'; - -Registry.as(Extensions.Configuration).registerConfiguration({ - ...editorConfigurationBaseNode, - properties: { - 'editor.codeActionWidget.showHeaders': { - type: 'boolean', - scope: ConfigurationScope.LANGUAGE_OVERRIDABLE, - description: nls.localize('showCodeActionHeaders', "Enable/disable showing group headers in the code action menu."), - default: true, - }, - } -}); diff --git a/src/vs/editor/contrib/codeAction/test/browser/codeActionModel.test.ts b/src/vs/editor/contrib/codeAction/test/browser/codeActionModel.test.ts index a2a688534d0..5205ab1a46b 100644 --- a/src/vs/editor/contrib/codeAction/test/browser/codeActionModel.test.ts +++ b/src/vs/editor/contrib/codeAction/test/browser/codeActionModel.test.ts @@ -9,15 +9,14 @@ import { assertType } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { Selection } from 'vs/editor/common/core/selection'; -import { TextModel } from 'vs/editor/common/model/textModel'; +import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; import * as languages from 'vs/editor/common/languages'; +import { TextModel } from 'vs/editor/common/model/textModel'; import { CodeActionModel, CodeActionsState } from 'vs/editor/contrib/codeAction/browser/codeActionModel'; import { createTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { MarkerService } from 'vs/platform/markers/common/markerService'; -import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; const testProvider = { provideCodeActions(): languages.CodeActionList { @@ -127,49 +126,6 @@ suite('CodeActionModel', () => { }); }); - test('Lightbulb is in the wrong place, #29933', async () => { - const reg = registry.register(languageId, { - provideCodeActions(_doc, _range): languages.CodeActionList { - return { actions: [], dispose() { /* noop*/ } }; - } - }); - disposables.add(reg); - - await runWithFakedTimers({ useFakeTimers: true }, async () => { - editor.getModel()!.setValue('// @ts-check\n2\ncon\n'); - - markerService.changeOne('fake', uri, [{ - startLineNumber: 3, startColumn: 1, endLineNumber: 3, endColumn: 4, - message: 'error', - severity: 1, - code: '', - source: '' - }]); - - // case 1 - drag selection over multiple lines -> range of enclosed marker, position or marker - await new Promise(resolve => { - const contextKeys = new MockContextKeyService(); - const model = disposables.add(new CodeActionModel(editor, registry, markerService, contextKeys, undefined)); - disposables.add(model.onDidChangeState((e: CodeActionsState.State) => { - assertType(e.type === CodeActionsState.Type.Triggered); - - assert.strictEqual(e.trigger.type, languages.CodeActionTriggerType.Auto); - const selection = e.rangeOrSelection; - assert.strictEqual(selection.selectionStartLineNumber, 1); - assert.strictEqual(selection.selectionStartColumn, 1); - assert.strictEqual(selection.endLineNumber, 4); - assert.strictEqual(selection.endColumn, 1); - assert.strictEqual(e.position.lineNumber, 3); - assert.strictEqual(e.position.column, 1); - model.dispose(); - resolve(undefined); - }, 5)); - - editor.setSelection({ startLineNumber: 1, startColumn: 1, endLineNumber: 4, endColumn: 1 }); - }); - }); - }); - test('Oracle -> should only auto trigger once for cursor and marker update right after each other', async () => { let done: () => void; const donePromise = new Promise(resolve => { done = resolve; }); diff --git a/src/vs/editor/contrib/copyPaste/browser/copyPasteController.ts b/src/vs/editor/contrib/copyPaste/browser/copyPasteController.ts index 115685c19e5..02fa00393c6 100644 --- a/src/vs/editor/contrib/copyPaste/browser/copyPasteController.ts +++ b/src/vs/editor/contrib/copyPaste/browser/copyPasteController.ts @@ -13,16 +13,15 @@ import { Mimes } from 'vs/base/common/mime'; import { generateUuid } from 'vs/base/common/uuid'; import { toVSDataTransfer, UriList } from 'vs/editor/browser/dnd'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { IBulkEditService, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { Handler, IEditorContribution, PastePayload } from 'vs/editor/common/editorCommon'; -import { DocumentPasteEdit, DocumentPasteEditProvider } from 'vs/editor/common/languages'; +import { DocumentPasteEdit, DocumentPasteEditProvider, WorkspaceEdit } from 'vs/editor/common/languages'; import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { CodeEditorStateFlag, EditorStateCancellationTokenSource } from 'vs/editor/contrib/editorState/browser/editorState'; -import { performSnippetEdit } from 'vs/editor/contrib/snippet/browser/snippetController2'; import { SnippetParser } from 'vs/editor/contrib/snippet/browser/snippetParser'; import { localize } from 'vs/nls'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; @@ -210,11 +209,18 @@ export class CopyPasteController extends Disposable implements IEditorContributi } if (providerEdit) { - performSnippetEdit(this._editor, typeof providerEdit.insertText === 'string' ? SnippetParser.escape(providerEdit.insertText) : providerEdit.insertText.snippet, selections); - - if (providerEdit.additionalEdit) { - await this._bulkEditService.apply(providerEdit.additionalEdit, { editor: this._editor }); - } + const snippet = typeof providerEdit.insertText === 'string' ? SnippetParser.escape(providerEdit.insertText) : providerEdit.insertText.snippet; + const combinedWorkspaceEdit: WorkspaceEdit = { + edits: [ + new ResourceTextEdit(model.uri, { + range: Selection.liftSelection(this._editor.getSelection()), + text: snippet, + insertAsSnippet: true, + }), + ...(providerEdit.additionalEdit?.edits ?? []) + ] + }; + await this._bulkEditService.apply(combinedWorkspaceEdit, { editor: this._editor }); return; } diff --git a/src/vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution.ts b/src/vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution.ts index ea283a30a37..1d72b9307f7 100644 --- a/src/vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution.ts +++ b/src/vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution.ts @@ -13,16 +13,14 @@ import { URI } from 'vs/base/common/uri'; import { addExternalEditorsDropData, toVSDataTransfer, UriList } from 'vs/editor/browser/dnd'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { IBulkEditService, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { IPosition } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { Selection, SelectionDirection } from 'vs/editor/common/core/selection'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; -import { DocumentOnDropEdit, DocumentOnDropEditProvider } from 'vs/editor/common/languages'; +import { DocumentOnDropEdit, DocumentOnDropEditProvider, WorkspaceEdit } from 'vs/editor/common/languages'; import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { CodeEditorStateFlag, EditorStateCancellationTokenSource } from 'vs/editor/contrib/editorState/browser/editorState'; -import { performSnippetEdit } from 'vs/editor/contrib/snippet/browser/snippetController2'; import { SnippetParser } from 'vs/editor/contrib/snippet/browser/snippetParser'; import { localize } from 'vs/nls'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; @@ -68,7 +66,7 @@ export class DropIntoEditorController extends Disposable implements IEditorContr try { const providers = this._languageFeaturesService.documentOnDropEditProvider.ordered(model); - const edit = await this._progressService.withProgress({ + const providerEdit = await this._progressService.withProgress({ location: ProgressLocation.Notification, delay: 750, title: localize('dropProgressTitle', "Running drop handlers..."), @@ -94,13 +92,19 @@ export class DropIntoEditorController extends Disposable implements IEditorContr return; } - if (edit) { - const range = new Range(position.lineNumber, position.column, position.lineNumber, position.column); - performSnippetEdit(editor, typeof edit.insertText === 'string' ? SnippetParser.escape(edit.insertText) : edit.insertText.snippet, [Selection.fromRange(range, SelectionDirection.LTR)]); - - if (edit.additionalEdit) { - await this._bulkEditService.apply(edit.additionalEdit, { editor }); - } + if (providerEdit) { + const snippet = typeof providerEdit.insertText === 'string' ? SnippetParser.escape(providerEdit.insertText) : providerEdit.insertText.snippet; + const combinedWorkspaceEdit: WorkspaceEdit = { + edits: [ + new ResourceTextEdit(model.uri, { + range: new Range(position.lineNumber, position.column, position.lineNumber, position.column), + text: snippet, + insertAsSnippet: true, + }), + ...(providerEdit.additionalEdit?.edits ?? []) + ] + }; + await this._bulkEditService.apply(combinedWorkspaceEdit, { editor }); return; } } finally { diff --git a/src/vs/editor/contrib/hover/browser/contentHover.ts b/src/vs/editor/contrib/hover/browser/contentHover.ts index 666337b5614..e827d2cec6c 100644 --- a/src/vs/editor/contrib/hover/browser/contentHover.ts +++ b/src/vs/editor/contrib/hover/browser/contentHover.ts @@ -34,9 +34,7 @@ export class ContentHoverController extends Disposable { private readonly _computer: ContentHoverComputer; private readonly _hoverOperation: HoverOperation; - private _messages: IHoverPart[]; - private _messagesAreComplete: boolean; - private _isChangingDecorations: boolean = false; + private _currentResult: HoverResult | null = null; constructor( private readonly _editor: ICodeEditor, @@ -45,9 +43,6 @@ export class ContentHoverController extends Disposable { ) { super(); - this._messages = []; - this._messagesAreComplete = false; - // Instantiate participants and sort them by `hoverOrdinal` which is relevant for rendering order. this._participants = []; for (const participant of HoverParticipantRegistry.getAll()) { @@ -59,13 +54,12 @@ export class ContentHoverController extends Disposable { this._hoverOperation = this._register(new HoverOperation(this._editor, this._computer)); this._register(this._hoverOperation.onResult((result) => { - this._withResult(result.value, result.isComplete, result.hasLoadingMessage); - })); - this._register(this._editor.onDidChangeModelDecorations(() => { - if (this._isChangingDecorations) { + if (!this._computer.anchor) { + // invalid state, ignore result return; } - this._onModelDecorationsChanged(); + const messages = (result.hasLoadingMessage ? this._addLoadingMessage(result.value) : result.value); + this._withResult(new HoverResult(this._computer.anchor, messages, result.isComplete)); })); this._register(dom.addStandardDisposableListener(this._widget.getDomNode(), 'keydown', (e) => { if (e.equals(KeyCode.Escape)) { @@ -73,25 +67,16 @@ export class ContentHoverController extends Disposable { } })); this._register(TokenizationRegistry.onDidChange(() => { - if (this._widget.position && this._computer.anchor && this._messages.length > 0) { + if (this._widget.position && this._currentResult) { this._widget.clear(); - this._renderMessages(this._computer.anchor, this._messages); + this._setCurrentResult(this._currentResult); // render again } })); } - private _onModelDecorationsChanged(): void { - if (this._widget.position) { - // The decorations have changed and the hover is visible, - // we need to recompute the displayed text - this._hoverOperation.cancel(); - - if (!this._widget.isColorPickerVisible) { // TODO@Michel ensure that displayed text for other decorations is computed even if color picker is in place - this._hoverOperation.start(HoverStartMode.Delayed); - } - } - } - + /** + * Returns true if the hover shows now or will show. + */ public maybeShowAt(mouseEvent: IEditorMouseEvent): boolean { const anchorCandidates: HoverAnchor[] = []; @@ -107,66 +92,111 @@ export class ContentHoverController extends Disposable { const target = mouseEvent.target; if (target.type === MouseTargetType.CONTENT_TEXT) { - anchorCandidates.push(new HoverRangeAnchor(0, target.range)); + anchorCandidates.push(new HoverRangeAnchor(0, target.range, mouseEvent.event.posx, mouseEvent.event.posy)); } if (target.type === MouseTargetType.CONTENT_EMPTY) { const epsilon = this._editor.getOption(EditorOption.fontInfo).typicalHalfwidthCharacterWidth / 2; if (!target.detail.isAfterLines && typeof target.detail.horizontalDistanceToText === 'number' && target.detail.horizontalDistanceToText < epsilon) { // Let hover kick in even when the mouse is technically in the empty area after a line, given the distance is small enough - anchorCandidates.push(new HoverRangeAnchor(0, target.range)); + anchorCandidates.push(new HoverRangeAnchor(0, target.range, mouseEvent.event.posx, mouseEvent.event.posy)); } } if (anchorCandidates.length === 0) { - return false; + return this._startShowingOrUpdateHover(null, HoverStartMode.Delayed, false, mouseEvent); } anchorCandidates.sort((a, b) => b.priority - a.priority); - this._startShowingAt(anchorCandidates[0], HoverStartMode.Delayed, false); - return true; + return this._startShowingOrUpdateHover(anchorCandidates[0], HoverStartMode.Delayed, false, mouseEvent); } public startShowingAtRange(range: Range, mode: HoverStartMode, focus: boolean): void { - this._startShowingAt(new HoverRangeAnchor(0, range), mode, focus); + this._startShowingOrUpdateHover(new HoverRangeAnchor(0, range, undefined, undefined), mode, focus, null); } - private _startShowingAt(anchor: HoverAnchor, mode: HoverStartMode, focus: boolean): void { + /** + * Returns true if the hover shows now or will show. + */ + private _startShowingOrUpdateHover(anchor: HoverAnchor | null, mode: HoverStartMode, focus: boolean, mouseEvent: IEditorMouseEvent | null): boolean { + if (!this._widget.position || !this._currentResult) { + // The hover is not visible + if (anchor) { + this._startHoverOperationIfNecessary(anchor, mode, focus, false); + return true; + } + return false; + } + + // The hover is currently visible + + const isGettingCloser = (mouseEvent && this._widget.isMouseGettingCloser(mouseEvent.event.posx, mouseEvent.event.posy)); + if (isGettingCloser) { + // The mouse is getting closer to the hover, so we will keep the hover untouched + // But we will kick off a hover update at the new anchor, insisting on keeping the hover visible. + if (anchor) { + this._startHoverOperationIfNecessary(anchor, mode, focus, true); + } + return true; + } + + if (!anchor) { + this._setCurrentResult(null); + return false; + } + + if (anchor && this._currentResult.anchor.equals(anchor)) { + // The widget is currently showing results for the exact same anchor, so no update is needed + return true; + } + + if (!anchor.canAdoptVisibleHover(this._currentResult.anchor, this._widget.position)) { + // The new anchor is not compatible with the previous anchor + this._setCurrentResult(null); + this._startHoverOperationIfNecessary(anchor, mode, focus, false); + return true; + } + + // We aren't getting any closer to the hover, so we will filter existing results + // and keep those which also apply to the new anchor. + this._setCurrentResult(this._currentResult.filter(anchor)); + this._startHoverOperationIfNecessary(anchor, mode, focus, false); + return true; + } + + private _startHoverOperationIfNecessary(anchor: HoverAnchor, mode: HoverStartMode, focus: boolean, insistOnKeepingHoverVisible: boolean): void { if (this._computer.anchor && this._computer.anchor.equals(anchor)) { - // We have to show the widget at the exact same range as before, so no work is needed + // We have to start a hover operation at the exact same anchor as before, so no work is needed return; } this._hoverOperation.cancel(); - - if (this._widget.position) { - // The range might have changed, but the hover is visible - // Instead of hiding it completely, filter out messages that are still in the new range and - // kick off a new computation - if (!this._computer.anchor || !anchor.canAdoptVisibleHover(this._computer.anchor, this._widget.position)) { - this.hide(); - } else { - const filteredMessages = this._messages.filter((m) => m.isValidForHoverAnchor(anchor)); - if (filteredMessages.length === 0) { - this.hide(); - } else if (filteredMessages.length === this._messages.length && this._messagesAreComplete) { - // no change - return; - } else { - this._renderMessages(anchor, filteredMessages); - } - } - } - this._computer.anchor = anchor; this._computer.shouldFocus = focus; + this._computer.insistOnKeepingHoverVisible = insistOnKeepingHoverVisible; this._hoverOperation.start(mode); } + private _setCurrentResult(hoverResult: HoverResult | null): void { + if (this._currentResult === hoverResult) { + // avoid updating the DOM to avoid resetting the user selection + return; + } + if (hoverResult && hoverResult.messages.length === 0) { + hoverResult = null; + } + this._currentResult = hoverResult; + if (this._currentResult) { + this._renderMessages(this._currentResult.anchor, this._currentResult.messages); + } else { + this._widget.hide(); + } + } + public hide(): void { this._computer.anchor = null; this._hoverOperation.cancel(); - this._widget.hide(); + this._setCurrentResult(null); } public isColorPickerVisible(): boolean { @@ -191,15 +221,22 @@ export class ContentHoverController extends Disposable { return result; } - private _withResult(result: IHoverPart[], isComplete: boolean, hasLoadingMessage: boolean): void { - this._messages = (hasLoadingMessage ? this._addLoadingMessage(result) : result); - this._messagesAreComplete = isComplete; + private _withResult(hoverResult: HoverResult): void { + if (this._widget.position && this._currentResult && this._currentResult.isComplete) { + // The hover is visible with a previous complete result. - if (this._computer.anchor && this._messages.length > 0) { - this._renderMessages(this._computer.anchor, this._messages); - } else if (isComplete) { - this.hide(); + if (!hoverResult.isComplete) { + // Instead of rendering the new partial result, we wait for the result to be complete. + return; + } + + if (this._computer.insistOnKeepingHoverVisible && hoverResult.messages.length === 0) { + // The hover would now hide normally, so we'll keep the previous messages + return; + } } + + this._setCurrentResult(hoverResult); } private _renderMessages(anchor: HoverAnchor, messages: IHoverPart[]): void { @@ -231,22 +268,12 @@ export class ContentHoverController extends Disposable { if (fragment.hasChildNodes()) { if (highlightRange) { const highlightDecoration = this._editor.createDecorationsCollection(); - try { - this._isChangingDecorations = true; - highlightDecoration.set([{ - range: highlightRange, - options: ContentHoverController._DECORATION_OPTIONS - }]); - } finally { - this._isChangingDecorations = false; - } + highlightDecoration.set([{ + range: highlightRange, + options: ContentHoverController._DECORATION_OPTIONS + }]); disposables.add(toDisposable(() => { - try { - this._isChangingDecorations = true; - highlightDecoration.clear(); - } finally { - this._isChangingDecorations = false; - } + highlightDecoration.clear(); })); } @@ -256,6 +283,8 @@ export class ContentHoverController extends Disposable { showAtRange, this._editor.getOption(EditorOption.hover).above, this._computer.shouldFocus, + anchor.initialMousePosX, + anchor.initialMousePosY, disposables )); } else { @@ -296,13 +325,51 @@ export class ContentHoverController extends Disposable { } } +class HoverResult { + + constructor( + public readonly anchor: HoverAnchor, + public readonly messages: IHoverPart[], + public readonly isComplete: boolean + ) { } + + public filter(anchor: HoverAnchor): HoverResult { + const filteredMessages = this.messages.filter((m) => m.isValidForHoverAnchor(anchor)); + if (filteredMessages.length === this.messages.length) { + return this; + } + return new FilteredHoverResult(this, this.anchor, filteredMessages, this.isComplete); + } +} + +class FilteredHoverResult extends HoverResult { + + constructor( + private readonly original: HoverResult, + anchor: HoverAnchor, + messages: IHoverPart[], + isComplete: boolean + ) { + super(anchor, messages, isComplete); + } + + public override filter(anchor: HoverAnchor): HoverResult { + return this.original.filter(anchor); + } +} + class ContentHoverVisibleData { + + public closestMouseDistance: number | undefined = undefined; + constructor( public readonly colorPicker: IEditorHoverColorPickerWidget | null, public readonly showAtPosition: Position, public readonly showAtRange: Range, public readonly preferAbove: boolean, public readonly stoleFocus: boolean, + public initialMousePosX: number | undefined, + public initialMousePosY: number | undefined, public readonly disposables: DisposableStore ) { } } @@ -383,6 +450,29 @@ export class ContentHoverWidget extends Disposable implements IContentWidget { }; } + public isMouseGettingCloser(posx: number, posy: number): boolean { + if (!this._visibleData) { + return false; + } + if (typeof this._visibleData.initialMousePosX === 'undefined' || typeof this._visibleData.initialMousePosY === 'undefined') { + this._visibleData.initialMousePosX = posx; + this._visibleData.initialMousePosY = posy; + return false; + } + + const widgetRect = dom.getDomNodePagePosition(this.getDomNode()); + if (typeof this._visibleData.closestMouseDistance === 'undefined') { + this._visibleData.closestMouseDistance = computeDistanceFromPointToRectangle(this._visibleData.initialMousePosX, this._visibleData.initialMousePosY, widgetRect.left, widgetRect.top, widgetRect.width, widgetRect.height); + } + const distance = computeDistanceFromPointToRectangle(posx, posy, widgetRect.left, widgetRect.top, widgetRect.width, widgetRect.height); + if (distance > this._visibleData.closestMouseDistance + 4 /* tolerance of 4 pixels */) { + // The mouse is getting farther away + return false; + } + this._visibleData.closestMouseDistance = Math.min(this._visibleData.closestMouseDistance, distance); + return true; + } + private _setVisibleData(visibleData: ContentHoverVisibleData | null): void { if (this._visibleData) { this._visibleData.disposables.dispose(); @@ -506,6 +596,10 @@ class ContentHoverComputer implements IHoverComputer { public get shouldFocus(): boolean { return this._shouldFocus; } public set shouldFocus(value: boolean) { this._shouldFocus = value; } + private _insistOnKeepingHoverVisible: boolean = false; + public get insistOnKeepingHoverVisible(): boolean { return this._insistOnKeepingHoverVisible; } + public set insistOnKeepingHoverVisible(value: boolean) { this._insistOnKeepingHoverVisible = value; } + constructor( private readonly _editor: ICodeEditor, private readonly _participants: readonly IEditorHoverParticipant[] @@ -581,3 +675,11 @@ class ContentHoverComputer implements IHoverComputer { return coalesce(result); } } + +function computeDistanceFromPointToRectangle(pointX: number, pointY: number, left: number, top: number, width: number, height: number): number { + const x = (left + width / 2); // x center of rectangle + const y = (top + height / 2); // y center of rectangle + const dx = Math.max(Math.abs(pointX - x) - width / 2, 0); + const dy = Math.max(Math.abs(pointY - y) - height / 2, 0); + return Math.sqrt(dx * dx + dy * dy); +} diff --git a/src/vs/editor/contrib/hover/browser/hoverTypes.ts b/src/vs/editor/contrib/hover/browser/hoverTypes.ts index 7dfa9e2a916..2f74d533f26 100644 --- a/src/vs/editor/contrib/hover/browser/hoverTypes.ts +++ b/src/vs/editor/contrib/hover/browser/hoverTypes.ts @@ -41,7 +41,9 @@ export class HoverRangeAnchor { public readonly type = HoverAnchorType.Range; constructor( public readonly priority: number, - public readonly range: Range + public readonly range: Range, + public readonly initialMousePosX: number | undefined, + public readonly initialMousePosY: number | undefined, ) { } public equals(other: HoverAnchor) { @@ -57,7 +59,9 @@ export class HoverForeignElementAnchor { constructor( public readonly priority: number, public readonly owner: IEditorHoverParticipant, - public readonly range: Range + public readonly range: Range, + public readonly initialMousePosX: number | undefined, + public readonly initialMousePosY: number | undefined, ) { } public equals(other: HoverAnchor) { diff --git a/src/vs/editor/contrib/hover/browser/markerHoverParticipant.ts b/src/vs/editor/contrib/hover/browser/markerHoverParticipant.ts index 813dad2d6b6..a4e1a5416e1 100644 --- a/src/vs/editor/contrib/hover/browser/markerHoverParticipant.ts +++ b/src/vs/editor/contrib/hover/browser/markerHoverParticipant.ts @@ -16,7 +16,7 @@ import { IModelDecoration } from 'vs/editor/common/model'; import { CodeActionTriggerType } from 'vs/editor/common/languages'; import { IMarkerDecorationsService } from 'vs/editor/common/services/markerDecorations'; import { CodeActionSet, getCodeActions } from 'vs/editor/contrib/codeAction/browser/codeAction'; -import { QuickFixAction, QuickFixController } from 'vs/editor/contrib/codeAction/browser/codeActionCommands'; +import { QuickFixAction, CodeActionController } from 'vs/editor/contrib/codeAction/browser/codeActionCommands'; import { CodeActionKind, CodeActionTrigger, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/browser/types'; import { MarkerController, NextMarkerAction } from 'vs/editor/contrib/gotoError/browser/gotoError'; import { HoverAnchor, HoverAnchorType, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; @@ -224,7 +224,7 @@ export class MarkerHoverParticipant implements IEditorHoverParticipant { showing = true; - const controller = QuickFixController.get(this._editor); + const controller = CodeActionController.get(this._editor); const elementPosition = dom.getDomNodePagePosition(target); // Hide the hover pre-emptively, otherwise the editor can close the code actions // context menu as well when using keyboard navigation diff --git a/src/vs/editor/contrib/inlayHints/browser/inlayHintsHover.ts b/src/vs/editor/contrib/inlayHints/browser/inlayHintsHover.ts index a4710cb752e..e04c6514d7a 100644 --- a/src/vs/editor/contrib/inlayHints/browser/inlayHintsHover.ts +++ b/src/vs/editor/contrib/inlayHints/browser/inlayHintsHover.ts @@ -26,8 +26,13 @@ import { asCommandLink } from 'vs/editor/contrib/inlayHints/browser/inlayHints'; import { isNonEmptyArray } from 'vs/base/common/arrays'; class InlayHintsHoverAnchor extends HoverForeignElementAnchor { - constructor(readonly part: RenderedInlayHintLabelPart, owner: InlayHintsHover) { - super(10, owner, part.item.anchor.range); + constructor( + readonly part: RenderedInlayHintLabelPart, + owner: InlayHintsHover, + initialMousePosX: number | undefined, + initialMousePosY: number | undefined + ) { + super(10, owner, part.item.anchor.range, initialMousePosX, initialMousePosY); } } @@ -58,7 +63,7 @@ export class InlayHintsHover extends MarkdownHoverParticipant implements IEditor if (!(options instanceof ModelDecorationInjectedTextOptions && options.attachedData instanceof RenderedInlayHintLabelPart)) { return null; } - return new InlayHintsHoverAnchor(options.attachedData, this); + return new InlayHintsHoverAnchor(options.attachedData, this, mouseEvent.event.posx, mouseEvent.event.posy); } override computeSync(): MarkdownHover[] { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/ghostTextHoverParticipant.ts b/src/vs/editor/contrib/inlineCompletions/browser/ghostTextHoverParticipant.ts index 6a2fe0eb945..46036f7f2f5 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/ghostTextHoverParticipant.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/ghostTextHoverParticipant.ts @@ -70,20 +70,20 @@ export class InlineCompletionsHoverParticipant implements IEditorHoverParticipan // handle the case where the mouse is over the view zone const viewZoneData = target.detail; if (controller.shouldShowHoverAtViewZone(viewZoneData.viewZoneId)) { - return new HoverForeignElementAnchor(1000, this, Range.fromPositions(viewZoneData.positionBefore || viewZoneData.position, viewZoneData.positionBefore || viewZoneData.position)); + return new HoverForeignElementAnchor(1000, this, Range.fromPositions(viewZoneData.positionBefore || viewZoneData.position, viewZoneData.positionBefore || viewZoneData.position), mouseEvent.event.posx, mouseEvent.event.posy); } } if (target.type === MouseTargetType.CONTENT_EMPTY) { // handle the case where the mouse is over the empty portion of a line following ghost text if (controller.shouldShowHoverAt(target.range)) { - return new HoverForeignElementAnchor(1000, this, target.range); + return new HoverForeignElementAnchor(1000, this, target.range, mouseEvent.event.posx, mouseEvent.event.posy); } } if (target.type === MouseTargetType.CONTENT_TEXT) { // handle the case where the mouse is directly over ghost text const mightBeForeignElement = target.detail.mightBeForeignElement; if (mightBeForeignElement && controller.shouldShowHoverAt(target.range)) { - return new HoverForeignElementAnchor(1000, this, target.range); + return new HoverForeignElementAnchor(1000, this, target.range, mouseEvent.event.posx, mouseEvent.event.posy); } } return null; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts index e6f2c1190d5..562960aa1ce 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts @@ -734,8 +734,26 @@ export async function provideInlineCompletions( snippetInfo = undefined; } else if ('snippet' in item.insertText) { + const preBracketCompletionLength = item.insertText.snippet.length; + + if (languageConfigurationService && item.completeBracketPairs) { + item.insertText.snippet = closeBrackets( + item.insertText.snippet, + range.getStartPosition(), + model, + languageConfigurationService + ); + + // Modify range depending on if brackets are added or removed + const diff = item.insertText.snippet.length - preBracketCompletionLength; + if (diff !== 0) { + range = new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn + diff); + } + } + const snippet = new SnippetParser().parse(item.insertText.snippet); insertText = snippet.toString(); + snippetInfo = { snippet: item.insertText.snippet, range: range diff --git a/src/vs/editor/contrib/snippet/browser/snippetController2.ts b/src/vs/editor/contrib/snippet/browser/snippetController2.ts index c3f773e8e9f..19187239da7 100644 --- a/src/vs/editor/contrib/snippet/browser/snippetController2.ts +++ b/src/vs/editor/contrib/snippet/browser/snippetController2.ts @@ -10,7 +10,6 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorCommand, registerEditorCommand, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { ISelection, Selection } from 'vs/editor/common/core/selection'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { CompletionItem, CompletionItemKind, CompletionItemProvider } from 'vs/editor/common/languages'; @@ -348,21 +347,3 @@ registerEditorCommand(new CommandCtor({ // primary: KeyCode.Enter, // } })); - - -// --- - -export function performSnippetEdit(editor: ICodeEditor, snippet: string, selections: ISelection[]): boolean { - const controller = SnippetController2.get(editor); - if (!controller) { - return false; - } - editor.focus(); - controller.apply(selections.map(selection => { - return { - range: Selection.liftSelection(selection), - template: snippet - }; - })); - return controller.isInSnippet(); -} diff --git a/src/vs/loader.js b/src/vs/loader.js index 0119744883d..67ef8665099 100644 --- a/src/vs/loader.js +++ b/src/vs/loader.js @@ -1926,7 +1926,7 @@ var AMDLoader; } if (env.isNode && !env.isElectronRenderer && !env.isElectronNodeIntegrationWebWorker) { module.exports = RequireFunc; - require = RequireFunc; + // require = RequireFunc; } else { if (!env.isElectronRenderer) { diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 8736a56f0b9..5c9c1a1a25c 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -5046,6 +5046,8 @@ declare namespace monaco.editor { export interface IMouseTargetOutsideEditor extends IBaseMouseTarget { readonly type: MouseTargetType.OUTSIDE_EDITOR; + readonly outsidePosition: 'above' | 'below' | 'left' | 'right'; + readonly outsideDistance: number; } /** diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 06a6b64bedd..e32071c164c 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Action, IAction, Separator, SubmenuAction } from 'vs/base/common/actions'; +import { Action, IAction, SubmenuAction } from 'vs/base/common/actions'; import { CSSIcon } from 'vs/base/common/codicons'; import { Event, MicrotaskEmitter } from 'vs/base/common/event'; import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -199,8 +199,15 @@ export interface IMenuActionOptions { renderShortTitle?: boolean; } +export interface IMenuChangeEvent { + readonly menu: IMenu; + readonly isStructuralChange: boolean; + readonly isToggleChange: boolean; + readonly isEnablementChange: boolean; +} + export interface IMenu extends IDisposable { - readonly onDidChange: Event; + readonly onDidChange: Event; getActions(options?: IMenuActionOptions): [string, Array][]; } @@ -374,28 +381,9 @@ export class SubmenuItemAction extends SubmenuAction { constructor( readonly item: ISubmenuItem, readonly hideActions: IMenuItemHide | undefined, - private readonly _menuService: IMenuService, - private readonly _contextKeyService: IContextKeyService, - private readonly _options?: IMenuActionOptions + actions: IAction[], ) { - super(`submenuitem.${item.submenu.id}`, typeof item.title === 'string' ? item.title : item.title.value, [], 'submenu'); - } - - override get actions(): readonly IAction[] { - const result: IAction[] = []; - const menu = this._menuService.createMenu(this.item.submenu, this._contextKeyService); - const groups = menu.getActions(this._options); - menu.dispose(); - for (const [, actions] of groups) { - if (actions.length > 0) { - result.push(...actions); - result.push(new Separator()); - } - } - if (result.length) { - result.pop(); // remove last separator - } - return result; + super(`submenuitem.${item.submenu.id}`, typeof item.title === 'string' ? item.title : item.title.value, actions, 'submenu'); } } diff --git a/src/vs/platform/actions/common/menuService.ts b/src/vs/platform/actions/common/menuService.ts index 134ae625399..86ff1bd3b7e 100644 --- a/src/vs/platform/actions/common/menuService.ts +++ b/src/vs/platform/actions/common/menuService.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { RunOnceScheduler } from 'vs/base/common/async'; -import { Emitter, Event } from 'vs/base/common/event'; +import { DebounceEmitter, Emitter, Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { IMenu, IMenuActionOptions, IMenuCreateOptions, IMenuItem, IMenuItemHide, IMenuService, isIMenuItem, isISubmenuItem, ISubmenuItem, MenuId, MenuItemAction, MenuRegistry, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { IMenu, IMenuActionOptions, IMenuChangeEvent, IMenuCreateOptions, IMenuItem, IMenuItemHide, IMenuService, isIMenuItem, isISubmenuItem, ISubmenuItem, MenuId, MenuItemAction, MenuRegistry, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { ICommandAction, ILocalizedString } from 'vs/platform/action/common/action'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { ContextKeyExpression, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IAction, toAction } from 'vs/base/common/actions'; +import { IAction, Separator, toAction } from 'vs/base/common/actions'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { removeFastWithoutKeepingOrder } from 'vs/base/common/arrays'; import { localize } from 'vs/nls'; @@ -29,7 +29,7 @@ export class MenuService implements IMenuService { } createMenu(id: MenuId, contextKeyService: IContextKeyService, options?: IMenuCreateOptions): IMenu { - return new Menu(id, this._hiddenStates, { emitEventsForSubmenuChanges: false, eventDebounceDelay: 50, ...options }, this._commandService, contextKeyService, this); + return new MenuImpl(id, this._hiddenStates, { emitEventsForSubmenuChanges: false, eventDebounceDelay: 50, ...options }, this._commandService, contextKeyService); } resetHiddenStates(id?: MenuId): void { @@ -135,81 +135,47 @@ class PersistedMenuHideState { type MenuItemGroup = [string, Array]; -class Menu implements IMenu { - - private readonly _disposables = new DisposableStore(); - - private readonly _onDidChange: Emitter; - readonly onDidChange: Event; +class MenuInfo { private _menuGroups: MenuItemGroup[] = []; - private _contextKeys: Set = new Set(); + private _structureContextKeys: Set = new Set(); + private _preconditionContextKeys: Set = new Set(); + private _toggledContextKeys: Set = new Set(); constructor( private readonly _id: MenuId, private readonly _hiddenStates: PersistedMenuHideState, - private readonly _options: Required, + private readonly _collectContextKeysForSubmenus: boolean, @ICommandService private readonly _commandService: ICommandService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @IMenuService private readonly _menuService: IMenuService ) { - this._build(); - - // Rebuild this menu whenever the menu registry reports an event for this MenuId. - // This usually happen while code and extensions are loaded and affects the over - // structure of the menu - const rebuildMenuSoon = new RunOnceScheduler(() => { - this._build(); - this._onDidChange.fire(this); - }, _options.eventDebounceDelay); - this._disposables.add(rebuildMenuSoon); - this._disposables.add(MenuRegistry.onDidChangeMenu(e => { - if (e.has(_id)) { - rebuildMenuSoon.schedule(); - } - })); - - // When context keys or storage state changes we need to check if the menu also has changed. However, - // we only do that when someone listens on this menu because (1) these events are - // firing often and (2) menu are often leaked - const lazyListener = this._disposables.add(new DisposableStore()); - const startLazyListener = () => { - const fireChangeSoon = new RunOnceScheduler(() => this._onDidChange.fire(this), _options.eventDebounceDelay); - lazyListener.add(fireChangeSoon); - lazyListener.add(_contextKeyService.onDidChangeContext(e => { - if (e.affectsSome(this._contextKeys)) { - fireChangeSoon.schedule(); - } - })); - lazyListener.add(_hiddenStates.onDidChange(() => { - fireChangeSoon.schedule(); - })); - }; - - this._onDidChange = new Emitter({ - // start/stop context key listener - onFirstListenerAdd: startLazyListener, - onLastListenerRemove: lazyListener.clear.bind(lazyListener) - }); - this.onDidChange = this._onDidChange.event; - + this.refresh(); } - dispose(): void { - this._disposables.dispose(); - this._onDidChange.dispose(); + get structureContextKeys(): ReadonlySet { + return this._structureContextKeys; } - private _build(): void { + get preconditionContextKeys(): ReadonlySet { + return this._preconditionContextKeys; + } + + get toggledContextKeys(): ReadonlySet { + return this._toggledContextKeys; + } + + refresh(): void { // reset this._menuGroups.length = 0; - this._contextKeys.clear(); + this._structureContextKeys.clear(); + this._preconditionContextKeys.clear(); + this._toggledContextKeys.clear(); const menuItems = MenuRegistry.getMenuItems(this._id); let group: MenuItemGroup | undefined; - menuItems.sort(Menu._compareMenuItems); + menuItems.sort(MenuInfo._compareMenuItems); for (const item of menuItems) { // group by groupId @@ -227,27 +193,27 @@ class Menu implements IMenu { private _collectContextKeys(item: IMenuItem | ISubmenuItem): void { - Menu._fillInKbExprKeys(item.when, this._contextKeys); + MenuInfo._fillInKbExprKeys(item.when, this._structureContextKeys); if (isIMenuItem(item)) { // keep precondition keys for event if applicable if (item.command.precondition) { - Menu._fillInKbExprKeys(item.command.precondition, this._contextKeys); + MenuInfo._fillInKbExprKeys(item.command.precondition, this._preconditionContextKeys); } // keep toggled keys for event if applicable if (item.command.toggled) { const toggledExpression: ContextKeyExpression = (item.command.toggled as { condition: ContextKeyExpression }).condition || item.command.toggled; - Menu._fillInKbExprKeys(toggledExpression, this._contextKeys); + MenuInfo._fillInKbExprKeys(toggledExpression, this._toggledContextKeys); } - } else if (this._options.emitEventsForSubmenuChanges) { + } else if (this._collectContextKeysForSubmenus) { // recursively collect context keys from submenus so that this // menu fires events when context key changes affect submenus MenuRegistry.getMenuItems(item.submenu).forEach(this._collectContextKeys, this); } } - getActions(options?: IMenuActionOptions): [string, Array][] { + createActionGroups(options: IMenuActionOptions | undefined): [string, Array][] { const result: [string, Array][] = []; const allToggleActions: IAction[][] = []; @@ -259,22 +225,20 @@ class Menu implements IMenu { const activeActions: Array = []; for (const item of items) { if (this._contextKeyService.contextMatchesRules(item.when)) { - let action: MenuItemAction | SubmenuItemAction | undefined; const isMenuItem = isIMenuItem(item); const menuHide = createMenuHide(this._id, isMenuItem ? item.command : item, this._hiddenStates); if (isMenuItem) { - action = new MenuItemAction(item.command, item.alt, options, menuHide, this._contextKeyService, this._commandService); + // MenuItemAction + activeActions.push(new MenuItemAction(item.command, item.alt, options, menuHide, this._contextKeyService, this._commandService)); } else { - action = new SubmenuItemAction(item, menuHide, this._menuService, this._contextKeyService, options); - if (action.actions.length === 0) { - action = undefined; + // SubmenuItemAction + const groups = new MenuInfo(item.submenu, this._hiddenStates, this._collectContextKeysForSubmenus, this._commandService, this._contextKeyService).createActionGroups(options); + const submenuActions = Separator.join(...groups.map(g => g[1])); + if (submenuActions.length > 0) { + activeActions.push(new SubmenuItemAction(item, menuHide, submenuActions)); } } - - if (action) { - activeActions.push(action); - } } } if (activeActions.length > 0) { @@ -333,7 +297,7 @@ class Menu implements IMenu { } // sort on titles - return Menu._compareTitles( + return MenuInfo._compareTitles( isIMenuItem(a) ? a.command.title : a.title, isIMenuItem(b) ? b.command.title : b.title ); @@ -346,6 +310,96 @@ class Menu implements IMenu { } } +class MenuImpl implements IMenu { + + private readonly _menuInfo: MenuInfo; + private readonly _disposables = new DisposableStore(); + + private readonly _onDidChange: Emitter; + readonly onDidChange: Event; + + constructor( + id: MenuId, + hiddenStates: PersistedMenuHideState, + options: Required, + @ICommandService commandService: ICommandService, + @IContextKeyService contextKeyService: IContextKeyService, + ) { + this._menuInfo = new MenuInfo(id, hiddenStates, options.emitEventsForSubmenuChanges, commandService, contextKeyService); + + // Rebuild this menu whenever the menu registry reports an event for this MenuId. + // This usually happen while code and extensions are loaded and affects the over + // structure of the menu + const rebuildMenuSoon = new RunOnceScheduler(() => { + this._menuInfo.refresh(); + this._onDidChange.fire({ menu: this, isStructuralChange: true, isEnablementChange: true, isToggleChange: true }); + }, options.eventDebounceDelay); + this._disposables.add(rebuildMenuSoon); + this._disposables.add(MenuRegistry.onDidChangeMenu(e => { + if (e.has(id)) { + rebuildMenuSoon.schedule(); + } + })); + + // When context keys or storage state changes we need to check if the menu also has changed. However, + // we only do that when someone listens on this menu because (1) these events are + // firing often and (2) menu are often leaked + const lazyListener = this._disposables.add(new DisposableStore()); + + const merge = (events: IMenuChangeEvent[]): IMenuChangeEvent => { + + let isStructuralChange = false; + let isEnablementChange = false; + let isToggleChange = false; + + for (const item of events) { + isStructuralChange = isStructuralChange || item.isStructuralChange; + isEnablementChange = isEnablementChange || item.isEnablementChange; + isToggleChange = isToggleChange || item.isToggleChange; + if (isStructuralChange && isEnablementChange && isToggleChange) { + // everything is TRUE, no need to continue iterating + break; + } + } + + return { menu: this, isStructuralChange, isEnablementChange, isToggleChange }; + }; + + const startLazyListener = () => { + + lazyListener.add(contextKeyService.onDidChangeContext(e => { + const isStructuralChange = e.affectsSome(this._menuInfo.structureContextKeys); + const isEnablementChange = e.affectsSome(this._menuInfo.preconditionContextKeys); + const isToggleChange = e.affectsSome(this._menuInfo.toggledContextKeys); + if (isStructuralChange || isEnablementChange || isToggleChange) { + this._onDidChange.fire({ menu: this, isStructuralChange, isEnablementChange, isToggleChange }); + } + })); + lazyListener.add(hiddenStates.onDidChange(e => { + this._onDidChange.fire({ menu: this, isStructuralChange: true, isEnablementChange: false, isToggleChange: false }); + })); + }; + + this._onDidChange = new DebounceEmitter({ + // start/stop context key listener + onFirstListenerAdd: startLazyListener, + onLastListenerRemove: lazyListener.clear.bind(lazyListener), + delay: options.eventDebounceDelay, + merge + }); + this.onDidChange = this._onDidChange.event; + } + + getActions(options?: IMenuActionOptions | undefined): [string, (MenuItemAction | SubmenuItemAction)[]][] { + return this._menuInfo.createActionGroups(options); + } + + dispose(): void { + this._disposables.dispose(); + this._onDidChange.dispose(); + } +} + function createMenuHide(menu: MenuId, command: ICommandAction | ISubmenuItem, states: PersistedMenuHideState): IMenuItemHide { const id = isISubmenuItem(command) ? command.submenu.id : command.id; diff --git a/src/vs/platform/configuration/common/configurationModels.ts b/src/vs/platform/configuration/common/configurationModels.ts index 7bac21c1972..804398fe3d3 100644 --- a/src/vs/platform/configuration/common/configurationModels.ts +++ b/src/vs/platform/configuration/common/configurationModels.ts @@ -27,10 +27,32 @@ export class ConfigurationModel implements IConfigurationModel { constructor( private readonly _contents: any = {}, private readonly _keys: string[] = [], - private readonly _overrides: IOverrides[] = [] + private readonly _overrides: IOverrides[] = [], + readonly raw?: ReadonlyArray | ConfigurationModel> ) { } + private _rawConfiguration: ConfigurationModel | undefined; + private get rawConfiguration(): ConfigurationModel { + if (!this._rawConfiguration) { + if (this.raw?.length) { + const rawConfigurationModels = this.raw.map(raw => { + if (raw instanceof ConfigurationModel) { + return raw; + } + const parser = new ConfigurationModelParser(''); + parser.parseRaw(raw); + return parser.configurationModel; + }); + this._rawConfiguration = rawConfigurationModels.reduce((previous, current) => current === previous ? current : previous.merge(current), rawConfigurationModels[0]); + } else { + // raw is same as current + this._rawConfiguration = this; + } + } + return this._rawConfiguration; + } + get contents(): any { return this.checkAndFreeze(this._contents); } @@ -55,6 +77,13 @@ export class ConfigurationModel implements IConfigurationModel { return section ? getConfigurationValue(this.contents, section) : this.contents; } + inspect(section: string | undefined, overrideIdentifier?: string | null): { value?: V; override?: V; merged?: V } { + const value = this.rawConfiguration.getValue(section); + const override = overrideIdentifier ? this.rawConfiguration.getOverrideValue(section, overrideIdentifier) : undefined; + const merged = overrideIdentifier ? this.rawConfiguration.override(overrideIdentifier).getValue(section) : value; + return { value, override, merged }; + } + getOverrideValue(section: string | undefined, overrideIdentifier: string): V | undefined { const overrideContents = this.getContentsForOverrideIdentifer(overrideIdentifier); return overrideContents @@ -93,8 +122,10 @@ export class ConfigurationModel implements IConfigurationModel { const contents = objects.deepClone(this.contents); const overrides = objects.deepClone(this.overrides); const keys = [...this.keys]; + const raws = this.raw?.length ? [...this.raw] : [this]; for (const other of others) { + raws.push(...(other.raw?.length ? other.raw : [other])); if (other.isEmpty()) { continue; } @@ -116,7 +147,7 @@ export class ConfigurationModel implements IConfigurationModel { } } } - return new ConfigurationModel(contents, keys, overrides); + return new ConfigurationModel(contents, keys, overrides, raws.every(raw => raw instanceof ConfigurationModel) ? undefined : raws); } freeze(): ConfigurationModel { @@ -284,8 +315,8 @@ export class ConfigurationModelParser { public parseRaw(raw: any, options?: ConfigurationParseOptions): void { this._raw = raw; - const { contents, keys, overrides, restricted } = this.doParseRaw(raw, options); - this._configurationModel = new ConfigurationModel(contents, keys, overrides); + const { contents, keys, overrides, restricted, hasExcludedProperties } = this.doParseRaw(raw, options); + this._configurationModel = new ConfigurationModel(contents, keys, overrides, hasExcludedProperties ? [raw] : undefined /* raw has not changed */); this._restrictedConfigurations = restricted || []; } @@ -346,19 +377,20 @@ export class ConfigurationModelParser { return raw; } - protected doParseRaw(raw: any, options?: ConfigurationParseOptions): IConfigurationModel & { restricted?: string[] } { + protected doParseRaw(raw: any, options?: ConfigurationParseOptions): IConfigurationModel & { restricted?: string[]; hasExcludedProperties?: boolean } { const configurationProperties = Registry.as(Extensions.Configuration).getConfigurationProperties(); const filtered = this.filter(raw, configurationProperties, true, options); raw = filtered.raw; const contents = toValuesTree(raw, message => console.error(`Conflict in settings file ${this._name}: ${message}`)); const keys = Object.keys(raw); const overrides = this.toOverrides(raw, message => console.error(`Conflict in settings file ${this._name}: ${message}`)); - return { contents, keys, overrides, restricted: filtered.restricted }; + return { contents, keys, overrides, restricted: filtered.restricted, hasExcludedProperties: filtered.hasExcludedProperties }; } - private filter(properties: any, configurationProperties: { [qualifiedKey: string]: IConfigurationPropertySchema | undefined }, filterOverriddenProperties: boolean, options?: ConfigurationParseOptions): { raw: {}; restricted: string[] } { + private filter(properties: any, configurationProperties: { [qualifiedKey: string]: IConfigurationPropertySchema | undefined }, filterOverriddenProperties: boolean, options?: ConfigurationParseOptions): { raw: {}; restricted: string[]; hasExcludedProperties: boolean } { + let hasExcludedProperties = false; if (!options?.scopes && !options?.skipRestricted) { - return { raw: properties, restricted: [] }; + return { raw: properties, restricted: [], hasExcludedProperties }; } const raw: any = {}; const restricted: string[] = []; @@ -366,6 +398,7 @@ export class ConfigurationModelParser { if (OVERRIDE_PROPERTY_REGEX.test(key) && filterOverriddenProperties) { const result = this.filter(properties[key], configurationProperties, false, options); raw[key] = result.raw; + hasExcludedProperties = hasExcludedProperties || result.hasExcludedProperties; restricted.push(...result.restricted); } else { const propertySchema = configurationProperties[key]; @@ -374,14 +407,15 @@ export class ConfigurationModelParser { restricted.push(key); } // Load unregistered configurations always. - if (scope === undefined || options.scopes === undefined || options.scopes.includes(scope)) { - if (!(options.skipRestricted && propertySchema?.restricted)) { - raw[key] = properties[key]; - } + if ((scope === undefined || options.scopes === undefined || options.scopes.includes(scope)) // Check scopes + && !(options.skipRestricted && propertySchema?.restricted)) { // Check restricted + raw[key] = properties[key]; + } else { + hasExcludedProperties = true; } } } - return { raw, restricted }; + return { raw, restricted, hasExcludedProperties }; } private toOverrides(raw: any, conflictReporter: (message: string) => void): IOverrides[] { @@ -501,39 +535,39 @@ export class Configuration { const folderConfigurationModel = this.getFolderConfigurationModelForResource(overrides.resource, workspace); const memoryConfigurationModel = overrides.resource ? this._memoryConfigurationByResource.get(overrides.resource) || this._memoryConfiguration : this._memoryConfiguration; - const defaultValue = overrides.overrideIdentifier ? this._defaultConfiguration.freeze().override(overrides.overrideIdentifier).getValue(key) : this._defaultConfiguration.freeze().getValue(key); - const policyValue = this._policyConfiguration.isEmpty() ? undefined : this._policyConfiguration.freeze().getValue(key); - const applicationValue = this.applicationConfiguration.isEmpty() ? undefined : this.applicationConfiguration.freeze().getValue(key); - const userValue = overrides.overrideIdentifier ? this.userConfiguration.freeze().override(overrides.overrideIdentifier).getValue(key) : this.userConfiguration.freeze().getValue(key); - const userLocalValue = overrides.overrideIdentifier ? this.localUserConfiguration.freeze().override(overrides.overrideIdentifier).getValue(key) : this.localUserConfiguration.freeze().getValue(key); - const userRemoteValue = overrides.overrideIdentifier ? this.remoteUserConfiguration.freeze().override(overrides.overrideIdentifier).getValue(key) : this.remoteUserConfiguration.freeze().getValue(key); - const workspaceValue = workspace ? overrides.overrideIdentifier ? this._workspaceConfiguration.freeze().override(overrides.overrideIdentifier).getValue(key) : this._workspaceConfiguration.freeze().getValue(key) : undefined; //Check on workspace exists or not because _workspaceConfiguration is never null - const workspaceFolderValue = folderConfigurationModel ? overrides.overrideIdentifier ? folderConfigurationModel.freeze().override(overrides.overrideIdentifier).getValue(key) : folderConfigurationModel.freeze().getValue(key) : undefined; - const memoryValue = overrides.overrideIdentifier ? memoryConfigurationModel.override(overrides.overrideIdentifier).getValue(key) : memoryConfigurationModel.getValue(key); + const defaultInspectValue = this._defaultConfiguration.inspect(key, overrides.overrideIdentifier); + const policyInspectValue = this._policyConfiguration.isEmpty() ? undefined : this._policyConfiguration.freeze().inspect(key); + const applicationInspectValue = this.applicationConfiguration.isEmpty() ? undefined : this.applicationConfiguration.freeze().inspect(key); + const userInspectValue = this.userConfiguration.freeze().inspect(key, overrides.overrideIdentifier); + const userLocalInspectValue = this.localUserConfiguration.freeze().inspect(key, overrides.overrideIdentifier); + const userRemoteInspectValue = this.remoteUserConfiguration.freeze().inspect(key, overrides.overrideIdentifier); + const workspaceInspectValue = workspace ? this._workspaceConfiguration.freeze().inspect(key, overrides.overrideIdentifier) : undefined; //Check on workspace exists or not because _workspaceConfiguration is never null + const workspaceFolderInspectValue = folderConfigurationModel ? folderConfigurationModel.freeze().inspect(key, overrides.overrideIdentifier) : undefined; + const memoryInspectValue = memoryConfigurationModel.inspect(key, overrides.overrideIdentifier); const value = consolidateConfigurationModel.getValue(key); const overrideIdentifiers: string[] = arrays.distinct(consolidateConfigurationModel.overrides.map(override => override.identifiers).flat()).filter(overrideIdentifier => consolidateConfigurationModel.getOverrideValue(key, overrideIdentifier) !== undefined); return { - defaultValue, - policyValue, - applicationValue, - userValue, - userLocalValue, - userRemoteValue, - workspaceValue, - workspaceFolderValue, - memoryValue, + defaultValue: defaultInspectValue ? defaultInspectValue.merged : undefined, + policyValue: policyInspectValue ? policyInspectValue.merged : undefined, + applicationValue: applicationInspectValue ? applicationInspectValue.merged : undefined, + userValue: userInspectValue ? userInspectValue.merged : undefined, + userLocalValue: userLocalInspectValue ? userLocalInspectValue.merged : undefined, + userRemoteValue: userRemoteInspectValue ? userRemoteInspectValue.merged : undefined, + workspaceValue: workspaceInspectValue ? workspaceInspectValue.merged : undefined, + workspaceFolderValue: workspaceFolderInspectValue ? workspaceFolderInspectValue.merged : undefined, + memoryValue: memoryInspectValue ? memoryInspectValue.merged : undefined, value, - default: defaultValue !== undefined ? { value: this._defaultConfiguration.freeze().getValue(key), override: overrides.overrideIdentifier ? this._defaultConfiguration.freeze().getOverrideValue(key, overrides.overrideIdentifier) : undefined } : undefined, - policy: policyValue !== undefined ? { value: policyValue } : undefined, - application: applicationValue !== undefined ? { value: applicationValue, override: overrides.overrideIdentifier ? this.applicationConfiguration.freeze().getOverrideValue(key, overrides.overrideIdentifier) : undefined } : undefined, - user: userValue !== undefined ? { value: this.userConfiguration.freeze().getValue(key), override: overrides.overrideIdentifier ? this.userConfiguration.freeze().getOverrideValue(key, overrides.overrideIdentifier) : undefined } : undefined, - userLocal: userLocalValue !== undefined ? { value: this.localUserConfiguration.freeze().getValue(key), override: overrides.overrideIdentifier ? this.localUserConfiguration.freeze().getOverrideValue(key, overrides.overrideIdentifier) : undefined } : undefined, - userRemote: userRemoteValue !== undefined ? { value: this.remoteUserConfiguration.freeze().getValue(key), override: overrides.overrideIdentifier ? this.remoteUserConfiguration.freeze().getOverrideValue(key, overrides.overrideIdentifier) : undefined } : undefined, - workspace: workspaceValue !== undefined ? { value: this._workspaceConfiguration.freeze().getValue(key), override: overrides.overrideIdentifier ? this._workspaceConfiguration.freeze().getOverrideValue(key, overrides.overrideIdentifier) : undefined } : undefined, - workspaceFolder: workspaceFolderValue !== undefined ? { value: folderConfigurationModel?.freeze().getValue(key), override: overrides.overrideIdentifier ? folderConfigurationModel?.freeze().getOverrideValue(key, overrides.overrideIdentifier) : undefined } : undefined, - memory: memoryValue !== undefined ? { value: memoryConfigurationModel.getValue(key), override: overrides.overrideIdentifier ? memoryConfigurationModel.getOverrideValue(key, overrides.overrideIdentifier) : undefined } : undefined, + default: defaultInspectValue.value !== undefined || defaultInspectValue.override !== undefined ? { value: defaultInspectValue.value, override: defaultInspectValue.override } : undefined, + policy: policyInspectValue?.value !== undefined ? { value: policyInspectValue.value } : undefined, + application: applicationInspectValue?.value !== undefined || applicationInspectValue?.override !== undefined ? { value: applicationInspectValue.value, override: applicationInspectValue.override } : undefined, + user: userInspectValue.value !== undefined || userInspectValue.override !== undefined ? { value: userInspectValue.value, override: userInspectValue.override } : undefined, + userLocal: userLocalInspectValue.value !== undefined || userLocalInspectValue.override !== undefined ? { value: userLocalInspectValue.value, override: userLocalInspectValue.override } : undefined, + userRemote: userRemoteInspectValue.value !== undefined || userRemoteInspectValue.override !== undefined ? { value: userRemoteInspectValue.value, override: userRemoteInspectValue.override } : undefined, + workspace: workspaceInspectValue?.value !== undefined || workspaceInspectValue?.override !== undefined ? { value: workspaceInspectValue.value, override: workspaceInspectValue.override } : undefined, + workspaceFolder: workspaceFolderInspectValue?.value !== undefined || workspaceFolderInspectValue?.override !== undefined ? { value: workspaceFolderInspectValue.value, override: workspaceFolderInspectValue.override } : undefined, + memory: memoryInspectValue.value !== undefined || memoryInspectValue.override !== undefined ? { value: memoryInspectValue.value, override: memoryInspectValue.override } : undefined, overrideIdentifiers: overrideIdentifiers.length ? overrideIdentifiers : undefined }; diff --git a/src/vs/platform/configuration/test/common/configurationModels.test.ts b/src/vs/platform/configuration/test/common/configurationModels.test.ts index cda244e99fa..6f6fd8f3724 100644 --- a/src/vs/platform/configuration/test/common/configurationModels.test.ts +++ b/src/vs/platform/configuration/test/common/configurationModels.test.ts @@ -304,6 +304,67 @@ suite('ConfigurationModel', () => { { identifiers: ['x'], contents: { 'a': 3, 'b': 2 }, keys: ['a', 'b'] }, ]); }); + + test('inspect when raw is same', () => { + const testObject = new ConfigurationModel({ 'a': 1, 'c': 1 }, ['a', 'c'], [{ identifiers: ['x', 'y'], contents: { 'a': 2, 'b': 1 }, keys: ['a'] }]); + + assert.deepStrictEqual(testObject.inspect('a'), { value: 1, override: undefined, merged: 1 }); + assert.deepStrictEqual(testObject.inspect('a', 'x'), { value: 1, override: 2, merged: 2 }); + assert.deepStrictEqual(testObject.inspect('b', 'x'), { value: undefined, override: 1, merged: 1 }); + assert.deepStrictEqual(testObject.inspect('d'), { value: undefined, override: undefined, merged: undefined }); + }); + + test('inspect when raw is not same', () => { + const testObject = new ConfigurationModel({ 'a': 1, 'c': 1 }, ['a', 'c'], [{ identifiers: ['x', 'y'], contents: { 'a': 2, }, keys: ['a'] }], [{ + 'a': 1, + 'b': 2, + 'c': 1, + 'd': 3, + '[x][y]': { + 'a': 2, + 'b': 1 + } + }]); + + assert.deepStrictEqual(testObject.inspect('a'), { value: 1, override: undefined, merged: 1 }); + assert.deepStrictEqual(testObject.inspect('a', 'x'), { value: 1, override: 2, merged: 2 }); + assert.deepStrictEqual(testObject.inspect('b', 'x'), { value: 2, override: 1, merged: 1 }); + assert.deepStrictEqual(testObject.inspect('d'), { value: 3, override: undefined, merged: 3 }); + assert.deepStrictEqual(testObject.inspect('e'), { value: undefined, override: undefined, merged: undefined }); + }); + + test('inspect in merged configuration when raw is same', () => { + const target1 = new ConfigurationModel({ 'a': 1 }, ['a'], [{ identifiers: ['x', 'y'], contents: { 'a': 2, }, keys: ['a'] }]); + const target2 = new ConfigurationModel({ 'b': 3 }, ['b'], []); + const testObject = target1.merge(target2); + + assert.deepStrictEqual(testObject.inspect('a'), { value: 1, override: undefined, merged: 1 }); + assert.deepStrictEqual(testObject.inspect('a', 'x'), { value: 1, override: 2, merged: 2 }); + assert.deepStrictEqual(testObject.inspect('b'), { value: 3, override: undefined, merged: 3 }); + assert.deepStrictEqual(testObject.inspect('b', 'y'), { value: 3, override: undefined, merged: 3 }); + assert.deepStrictEqual(testObject.inspect('c'), { value: undefined, override: undefined, merged: undefined }); + }); + + test('inspect in merged configuration when raw is not same for one model', () => { + const target1 = new ConfigurationModel({ 'a': 1 }, ['a'], [{ identifiers: ['x', 'y'], contents: { 'a': 2, }, keys: ['a'] }], [{ + 'a': 1, + 'b': 2, + 'c': 3, + '[x][y]': { + 'a': 2, + 'b': 4, + } + }]); + const target2 = new ConfigurationModel({ 'b': 3 }, ['b'], []); + const testObject = target1.merge(target2); + + assert.deepStrictEqual(testObject.inspect('a'), { value: 1, override: undefined, merged: 1 }); + assert.deepStrictEqual(testObject.inspect('a', 'x'), { value: 1, override: 2, merged: 2 }); + assert.deepStrictEqual(testObject.inspect('b'), { value: 3, override: undefined, merged: 3 }); + assert.deepStrictEqual(testObject.inspect('b', 'y'), { value: 3, override: 4, merged: 4 }); + assert.deepStrictEqual(testObject.inspect('c'), { value: 3, override: undefined, merged: 3 }); + }); + }); suite('CustomConfigurationModel', () => { diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index 26ef9dad17b..cf59e2f135a 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -15,7 +15,7 @@ export class NativeEnvironmentService extends AbstractNativeEnvironmentService { super(args, { homeDir: homedir(), tmpDir: tmpdir(), - userDataDir: getUserDataPath(args) + userDataDir: getUserDataPath(args, productService.nameShort) }, productService); } } diff --git a/src/vs/platform/environment/node/userDataPath.d.ts b/src/vs/platform/environment/node/userDataPath.d.ts index 4c9239fc953..c75b432c59c 100644 --- a/src/vs/platform/environment/node/userDataPath.d.ts +++ b/src/vs/platform/environment/node/userDataPath.d.ts @@ -11,4 +11,4 @@ import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; * - respect VSCODE_APPDATA environment variable * - respect --user-data-dir CLI argument */ -export function getUserDataPath(args: NativeParsedArgs): string; +export function getUserDataPath(args: NativeParsedArgs, productName: string): string; diff --git a/src/vs/platform/environment/node/userDataPath.js b/src/vs/platform/environment/node/userDataPath.js index 96639b2b44e..92898523ed1 100644 --- a/src/vs/platform/environment/node/userDataPath.js +++ b/src/vs/platform/environment/node/userDataPath.js @@ -14,18 +14,18 @@ * * @param {typeof import('path')} path * @param {typeof import('os')} os - * @param {string} productName * @param {string} cwd */ - function factory(path, os, productName, cwd) { + function factory(path, os, cwd) { /** * @param {NativeParsedArgs} cliArgs + * @param {string} productName * * @returns {string} */ - function getUserDataPath(cliArgs) { - const userDataPath = doGetUserDataPath(cliArgs); + function getUserDataPath(cliArgs, productName) { + const userDataPath = doGetUserDataPath(cliArgs, productName); const pathsToResolve = [userDataPath]; // If the user-data-path is not absolute, make @@ -43,10 +43,11 @@ /** * @param {NativeParsedArgs} cliArgs + * @param {string} productName * * @returns {string} */ - function doGetUserDataPath(cliArgs) { + function doGetUserDataPath(cliArgs, productName) { // 0. Running out of sources has a fixed productName if (process.env['VSCODE_DEV']) { @@ -106,25 +107,18 @@ } if (typeof define === 'function') { - define(['require', 'path', 'os', 'vs/base/common/network', 'vs/base/common/resources', 'vs/base/common/process'], function ( - require, + define(['path', 'os', 'vs/base/common/process'], function ( /** @type {typeof import('path')} */ path, /** @type {typeof import('os')} */ os, - /** @type {typeof import('../../../base/common/network')} */ network, - /** @type {typeof import("../../../base/common/resources")} */ resources, /** @type {typeof import("../../../base/common/process")} */ process ) { - const rootPath = resources.dirname(network.FileAccess.asFileUri('', require)); - const pkg = require.__$__nodeRequire(resources.joinPath(rootPath, 'package.json').fsPath); - - return factory(path, os, pkg.name, process.cwd()); - }); // amd + return factory(path, os, process.cwd()); // amd + }); } else if (typeof module === 'object' && typeof module.exports === 'object') { - const pkg = require('../../../../../package.json'); const path = require('path'); const os = require('os'); - module.exports = factory(path, os, pkg.name, process.env['VSCODE_CWD'] || process.cwd()); // commonjs + module.exports = factory(path, os, process.env['VSCODE_CWD'] || process.cwd()); // commonjs } else { throw new Error('Unknown context'); } diff --git a/src/vs/platform/environment/test/node/userDataPath.test.ts b/src/vs/platform/environment/test/node/userDataPath.test.ts index e0eb596af9f..f92f1808481 100644 --- a/src/vs/platform/environment/test/node/userDataPath.test.ts +++ b/src/vs/platform/environment/test/node/userDataPath.test.ts @@ -6,11 +6,12 @@ import * as assert from 'assert'; import { OPTIONS, parseArgs } from 'vs/platform/environment/node/argv'; import { getUserDataPath } from 'vs/platform/environment/node/userDataPath'; +import product from 'vs/platform/product/common/product'; suite('User data path', () => { test('getUserDataPath - default', () => { - const path = getUserDataPath(parseArgs(process.argv, OPTIONS)); + const path = getUserDataPath(parseArgs(process.argv, OPTIONS), product.nameShort); assert.ok(path.length > 0); }); @@ -20,7 +21,7 @@ suite('User data path', () => { const portableDir = 'portable-dir'; process.env['VSCODE_PORTABLE'] = portableDir; - const path = getUserDataPath(parseArgs(process.argv, OPTIONS)); + const path = getUserDataPath(parseArgs(process.argv, OPTIONS), product.nameShort); assert.ok(path.includes(portableDir)); } finally { if (typeof origPortable === 'string') { @@ -36,7 +37,7 @@ suite('User data path', () => { const args = parseArgs(process.argv, OPTIONS); args['user-data-dir'] = cliUserDataDir; - const path = getUserDataPath(args); + const path = getUserDataPath(args, product.nameShort); assert.ok(path.includes(cliUserDataDir)); }); @@ -46,7 +47,7 @@ suite('User data path', () => { const appDataDir = 'appdata-dir'; process.env['VSCODE_APPDATA'] = appDataDir; - const path = getUserDataPath(parseArgs(process.argv, OPTIONS)); + const path = getUserDataPath(parseArgs(process.argv, OPTIONS), product.nameShort); assert.ok(path.includes(appDataDir)); } finally { if (typeof origAppData === 'string') { diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 3515dea4464..e523486bdbd 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -301,6 +301,7 @@ class Query { get sortBy(): number { return this.state.sortBy; } get sortOrder(): number { return this.state.sortOrder; } get flags(): number { return this.state.flags; } + get criteria(): ICriterium[] { return this.state.criteria; } withPage(pageNumber: number, pageSize: number = this.state.pageSize): Query { return new Query({ ...this.state, pageNumber, pageSize }); @@ -566,8 +567,9 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi declare readonly _serviceBrand: undefined; - private extensionsGalleryUrl: string | undefined; - private extensionsControlUrl: string | undefined; + private readonly extensionsGalleryUrl: string | undefined; + private readonly extensionsGallerySearchUrl: string | undefined; + private readonly extensionsControlUrl: string | undefined; private readonly commonHeadersPromise: Promise<{ [key: string]: string }>; @@ -582,8 +584,9 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi @IConfigurationService private readonly configurationService: IConfigurationService, ) { const config = productService.extensionsGallery; - this.extensionsGalleryUrl = config && config.serviceUrl; - this.extensionsControlUrl = config && config.controlUrl; + this.extensionsGalleryUrl = config?.serviceUrl; + this.extensionsGallerySearchUrl = config?.searchUrl; + this.extensionsControlUrl = config?.controlUrl; this.commonHeadersPromise = resolveMarketplaceHeaders( productService.version, productService, @@ -944,7 +947,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi try { context = await this.requestService.request({ type: 'POST', - url: this.api('/extensionquery'), + url: this.extensionsGallerySearchUrl && query.criteria.some(c => c.filterType === FilterType.SearchText) ? this.extensionsGallerySearchUrl : this.api('/extensionquery'), data, headers }, token); diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 15172d6a143..e8a8e731abb 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -299,7 +299,6 @@ class ExtensionsScanner extends Disposable { } await this.extractAtLocation(extensionKey, zipPath, tempPath, token); - metadata.installedTimestamp = Date.now(); await this.extensionsScannerService.updateMetadata(URI.file(tempPath), metadata); try { @@ -617,6 +616,7 @@ class InstallGalleryExtensionTask extends InstallExtensionTask { isSystem: existingExtension?.type === ExtensionType.System ? true : undefined, updated: !!existingExtension, isPreReleaseVersion: this.gallery.properties.isPreReleaseVersion, + installedTimestamp: Date.now(), preRelease: this.gallery.properties.isPreReleaseVersion || (isBoolean(this.options.installPreReleaseVersion) ? this.options.installPreReleaseVersion /* Respect the passed flag */ @@ -691,6 +691,7 @@ class InstallVSIXTask extends InstallExtensionTask { metadata.isApplicationScoped = isApplicationScopedExtension(this.manifest); metadata.isMachineScoped = this.options.isMachineScoped || existing?.isMachineScoped; metadata.isBuiltin = this.options.isBuiltin || existing?.isBuiltin; + metadata.installedTimestamp = Date.now(); if (existing) { this._operation = InstallOperation.Update; diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index 011d3fbee14..267435d090a 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -268,7 +268,9 @@ export interface IRelaxedExtensionManifest { description?: string; main?: string; browser?: string; - l10nBundleLocation?: string; + // For now this only supports pointing to l10n bundle files + // but it will be used for package.l10n.json files in the future + l10n?: string; icon?: string; categories?: string[]; keywords?: string[]; diff --git a/src/vs/platform/extensions/electron-main/extensionHostStarter.ts b/src/vs/platform/extensions/electron-main/extensionHostStarter.ts index 9da8b146c18..78ea7967623 100644 --- a/src/vs/platform/extensions/electron-main/extensionHostStarter.ts +++ b/src/vs/platform/extensions/electron-main/extensionHostStarter.ts @@ -424,6 +424,7 @@ class UtilityExtensionHostProcess extends Disposable { const args: string[] = ['--type=extensionHost', '--skipWorkspaceStorageLock']; const execArgv: string[] = opts.execArgv || []; const env: { [key: string]: any } = { ...opts.env }; + const allowLoadingUnsignedLibraries: boolean = true; // Make sure all values are strings, otherwise the process will not start for (const key of Object.keys(env)) { @@ -432,7 +433,7 @@ class UtilityExtensionHostProcess extends Disposable { this._logService.info(`UtilityProcess<${this.id}>: Creating new...`); - this._process = new UtilityProcess(modulePath, args, { serviceName, env, execArgv }); + this._process = new UtilityProcess(modulePath, args, { serviceName, env, execArgv, allowLoadingUnsignedLibraries }); const stdoutDecoder = new StringDecoder('utf-8'); this._process.stdout?.on('data', (chunk) => { diff --git a/src/vs/platform/files/node/diskFileSystemProvider.ts b/src/vs/platform/files/node/diskFileSystemProvider.ts index 708221073ac..fffaaec83d5 100644 --- a/src/vs/platform/files/node/diskFileSystemProvider.ts +++ b/src/vs/platform/files/node/diskFileSystemProvider.ts @@ -48,6 +48,8 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileCloneCapability { + private static TRACE_LOG_RESOURCE_LOCKS = false; // not enabled by default because very spammy + constructor( logService: ILogService, options?: IDiskFileSystemProviderOptions @@ -156,14 +158,14 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple private async createResourceLock(resource: URI): Promise { const filePath = this.toFilePath(resource); - this.logService.trace(`[Disk FileSystemProvider]: createResourceLock() - request to acquire resource lock (${filePath})`); + this.traceLock(`[Disk FileSystemProvider]: createResourceLock() - request to acquire resource lock (${filePath})`); // Await pending locks for resource. It is possible for a new lock being // added right after opening, so we have to loop over locks until no lock // remains. let existingLock: Barrier | undefined = undefined; while (existingLock = this.resourceLocks.get(resource)) { - this.logService.trace(`[Disk FileSystemProvider]: createResourceLock() - waiting for resource lock to be released (${filePath})`); + this.traceLock(`[Disk FileSystemProvider]: createResourceLock() - waiting for resource lock to be released (${filePath})`); await existingLock.wait(); } @@ -171,19 +173,19 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple const newLock = new Barrier(); this.resourceLocks.set(resource, newLock); - this.logService.trace(`[Disk FileSystemProvider]: createResourceLock() - new resource lock created (${filePath})`); + this.traceLock(`[Disk FileSystemProvider]: createResourceLock() - new resource lock created (${filePath})`); return toDisposable(() => { - this.logService.trace(`[Disk FileSystemProvider]: createResourceLock() - resource lock dispose() (${filePath})`); + this.traceLock(`[Disk FileSystemProvider]: createResourceLock() - resource lock dispose() (${filePath})`); // Delete lock if it is still ours if (this.resourceLocks.get(resource) === newLock) { - this.logService.trace(`[Disk FileSystemProvider]: createResourceLock() - resource lock removed from resource-lock map (${filePath})`); + this.traceLock(`[Disk FileSystemProvider]: createResourceLock() - resource lock removed from resource-lock map (${filePath})`); this.resourceLocks.delete(resource); } // Open lock - this.logService.trace(`[Disk FileSystemProvider]: createResourceLock() - resource lock barrier open() (${filePath})`); + this.traceLock(`[Disk FileSystemProvider]: createResourceLock() - resource lock barrier open() (${filePath})`); newLock.open(); }); } @@ -192,7 +194,7 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple let lock: IDisposable | undefined = undefined; try { if (options?.atomic) { - this.logService.trace(`[Disk FileSystemProvider]: atomic read operation started (${this.toFilePath(resource)})`); + this.traceLock(`[Disk FileSystemProvider]: atomic read operation started (${this.toFilePath(resource)})`); // When the read should be atomic, make sure // to await any pending locks for the resource @@ -210,6 +212,12 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple } } + private traceLock(msg: string): void { + if (DiskFileSystemProvider.TRACE_LOG_RESOURCE_LOCKS) { + this.logService.trace(msg); + } + } + readFileStream(resource: URI, opts: IFileReadStreamOptions, token: CancellationToken): ReadableStreamEvents { const stream = newWriteableStream(data => VSBuffer.concat(data.map(data => VSBuffer.wrap(data))).buffer); @@ -359,7 +367,7 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple const previousLock = this.mapHandleToLock.get(fd); // Remember that this handle has an associated lock - this.logService.trace(`[Disk FileSystemProvider]: open() - storing lock for handle ${fd} (${filePath})`); + this.traceLock(`[Disk FileSystemProvider]: open() - storing lock for handle ${fd} (${filePath})`); this.mapHandleToLock.set(fd, lock); // There is a slight chance that a resource lock for a @@ -369,7 +377,7 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple // wise we end up in a deadlock situation // https://github.com/microsoft/vscode/issues/142462 if (previousLock) { - this.logService.trace(`[Disk FileSystemProvider]: open() - disposing a previous lock that was still stored on same handle ${fd} (${filePath})`); + this.traceLock(`[Disk FileSystemProvider]: open() - disposing a previous lock that was still stored on same handle ${fd} (${filePath})`); previousLock.dispose(); } } @@ -411,11 +419,11 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple } finally { if (lockForHandle) { if (this.mapHandleToLock.get(fd) === lockForHandle) { - this.logService.trace(`[Disk FileSystemProvider]: close() - resource lock removed from handle-lock map ${fd}`); + this.traceLock(`[Disk FileSystemProvider]: close() - resource lock removed from handle-lock map ${fd}`); this.mapHandleToLock.delete(fd); // only delete from map if this is still our lock! } - this.logService.trace(`[Disk FileSystemProvider]: close() - disposing lock for handle ${fd}`); + this.traceLock(`[Disk FileSystemProvider]: close() - disposing lock for handle ${fd}`); lockForHandle.dispose(); } } diff --git a/src/vs/platform/issue/electron-main/issueMainService.ts b/src/vs/platform/issue/electron-main/issueMainService.ts index 0294b6062b6..a26823ec79c 100644 --- a/src/vs/platform/issue/electron-main/issueMainService.ts +++ b/src/vs/platform/issue/electron-main/issueMainService.ts @@ -228,7 +228,7 @@ export class IssueMainService implements IIssueMainService { }); this.issueReporterWindow.loadURL( - FileAccess.asBrowserUri('vs/code/electron-sandbox/issue/issueReporter.html', require).toString(true) + FileAccess.asBrowserUri(`vs/code/electron-sandbox/issue/issueReporter${this.environmentMainService.isBuilt ? '' : '-dev'}.html`, require).toString(true) ); this.issueReporterWindow.on('close', () => { @@ -279,7 +279,7 @@ export class IssueMainService implements IIssueMainService { }); this.processExplorerWindow.loadURL( - FileAccess.asBrowserUri('vs/code/electron-sandbox/processExplorer/processExplorer.html', require).toString(true) + FileAccess.asBrowserUri(`vs/code/electron-sandbox/processExplorer/processExplorer${this.environmentMainService.isBuilt ? '' : '-dev'}.html`, require).toString(true) ); this.processExplorerWindow.on('close', () => { diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts index f0808b12783..bceda016f20 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts @@ -32,7 +32,6 @@ else if (typeof require?.__$__nodeRequire === 'function') { const rootPath = dirname(FileAccess.asFileUri('', require)); product = require.__$__nodeRequire(joinPath(rootPath, 'product.json').fsPath); - const pkg = require.__$__nodeRequire(joinPath(rootPath, 'package.json').fsPath) as { version: string }; // Running out of sources if (env['VSCODE_DEV']) { @@ -44,9 +43,16 @@ else if (typeof require?.__$__nodeRequire === 'function') { }); } - Object.assign(product, { - version: pkg.version - }); + // Version is added during built time, but we still + // want to have it running out of sources so we + // read it from package.json only when we need it. + if (!product.version) { + const pkg = require.__$__nodeRequire(joinPath(rootPath, 'package.json').fsPath) as { version: string }; + + Object.assign(product, { + version: pkg.version + }); + } } // Web environment or unknown diff --git a/src/vs/platform/protocol/electron-main/protocolMainService.ts b/src/vs/platform/protocol/electron-main/protocolMainService.ts index 4ebb88fe739..b4b9544fccf 100644 --- a/src/vs/platform/protocol/electron-main/protocolMainService.ts +++ b/src/vs/platform/protocol/electron-main/protocolMainService.ts @@ -96,7 +96,7 @@ export class ProtocolMainService extends Disposable implements IProtocolMainServ let headers: Record | undefined; if (this.environmentService.crossOriginIsolated) { - if (path.endsWith('/workbench.html')) { + if (path.endsWith('/workbench.html') || path.endsWith('/workbench-dev.html')) { headers = COI.CoopAndCoep; } else { headers = COI.getHeadersFromQuery(request.url); diff --git a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts index 4b9dbc3a765..5b3e3936011 100644 --- a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts +++ b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts @@ -250,7 +250,7 @@ export class SharedProcess extends Disposable implements ISharedProcess { }); // Load with config - this.window.loadURL(FileAccess.asBrowserUri('vs/code/electron-browser/sharedProcess/sharedProcess.html', require).toString(true)); + this.window.loadURL(FileAccess.asBrowserUri(`vs/code/electron-browser/sharedProcess/sharedProcess${this.environmentMainService.isBuilt ? '' : '-dev'}.html`, require).toString(true)); } private registerWindowListeners(): void { diff --git a/src/vs/platform/storage/test/electron-main/storageMainService.test.ts b/src/vs/platform/storage/test/electron-main/storageMainService.test.ts index 8d4d2971b6b..3e1fdf56baa 100644 --- a/src/vs/platform/storage/test/electron-main/storageMainService.test.ts +++ b/src/vs/platform/storage/test/electron-main/storageMainService.test.ts @@ -33,6 +33,7 @@ suite('StorageMainService', function () { const inMemoryProfile: IUserDataProfile = { id: 'id', name: 'inMemory', + shortName: 'inMemory', isDefault: false, location: inMemoryProfileRoot, globalStorageHome: joinPath(inMemoryProfileRoot, 'globalStorageHome'), diff --git a/src/vs/platform/terminal/common/capabilities/capabilities.ts b/src/vs/platform/terminal/common/capabilities/capabilities.ts index e2e9f8bab98..5b00519a62d 100644 --- a/src/vs/platform/terminal/common/capabilities/capabilities.ts +++ b/src/vs/platform/terminal/common/capabilities/capabilities.ts @@ -220,6 +220,7 @@ export interface ITerminalCommand { commandStartLineContent?: string; markProperties?: IMarkProperties; getOutput(): string | undefined; + getOutputMatch(outputMatcher: { lineMatcher: string | RegExp; anchor?: 'top' | 'bottom'; offset?: number; length?: number }): RegExpMatchArray | undefined; hasOutput(): boolean; } diff --git a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts index 1d268538a0a..48606b7fafa 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts @@ -8,6 +8,7 @@ import { debounce } from 'vs/base/common/decorators'; import { Emitter } from 'vs/base/common/event'; import { ILogService } from 'vs/platform/log/common/log'; import { ICommandDetectionCapability, TerminalCapability, ITerminalCommand, IHandleCommandOptions, ICommandInvalidationRequest, CommandInvalidationReason, ISerializedCommand, ISerializedCommandDetectionCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; + // Importing types is safe in any layer // eslint-disable-next-line local/code-import-patterns import type { IBuffer, IBufferLine, IDisposable, IMarker, Terminal } from 'xterm-headless'; @@ -485,6 +486,7 @@ export class CommandDetectionCapability implements ICommandDetectionCapability { commandStartLineContent: this._currentCommand.commandStartLineContent, hasOutput: () => !executedMarker?.isDisposed && !endMarker?.isDisposed && !!(executedMarker && endMarker && executedMarker?.line < endMarker!.line), getOutput: () => getOutputForCommand(executedMarker, endMarker, buffer), + getOutputMatch: (outputMatcher?: { lineMatcher: string | RegExp; anchor?: 'top' | 'bottom'; offset?: number; length?: number }) => getOutputMatchForCommand(executedMarker, endMarker, buffer, this._terminal.cols, outputMatcher), markProperties: options?.markProperties }; this._commands.push(newCommand); @@ -609,6 +611,7 @@ export class CommandDetectionCapability implements ICommandDetectionCapability { exitCode: e.exitCode, hasOutput: () => !executedMarker?.isDisposed && !endMarker?.isDisposed && !!(executedMarker && endMarker && executedMarker.line < endMarker.line), getOutput: () => getOutputForCommand(executedMarker, endMarker, buffer), + getOutputMatch: (outputMatcher: { lineMatcher: string | RegExp; anchor?: 'top' | 'bottom'; offset?: number; length?: number }) => getOutputMatchForCommand(executedMarker, endMarker, buffer, this._terminal.cols, outputMatcher), markProperties: e.markProperties }; this._commands.push(newCommand); @@ -639,3 +642,59 @@ function getOutputForCommand(executedMarker: IMarker | undefined, endMarker: IMa } return output === '' ? undefined : output; } + +export function getOutputMatchForCommand(executedMarker: IMarker | undefined, endMarker: IMarker | undefined, buffer: IBuffer, cols: number, outputMatcher: { lineMatcher: string | RegExp; anchor?: 'top' | 'bottom'; offset?: number; length?: number } | undefined): RegExpMatchArray | undefined { + if (!executedMarker || !endMarker) { + return undefined; + } + const startLine = executedMarker.line; + const endLine = endMarker.line; + + if (startLine === endLine) { + return undefined; + } + if (outputMatcher?.length && (endLine - startLine) < outputMatcher.length) { + return undefined; + } + let output = ''; + let line: string | undefined; + if (outputMatcher?.anchor === 'bottom') { + for (let i = endLine - (outputMatcher.offset || 0); i >= startLine; i--) { + line = getXtermLineContent(buffer, i, i, cols); + output = line + output; + const match = output.match(outputMatcher.lineMatcher); + if (match) { + return match; + } + } + } else { + for (let i = startLine + (outputMatcher?.offset || 0); i < endLine; i++) { + line = getXtermLineContent(buffer, i, i, cols); + output += line; + if (outputMatcher) { + const match = output.match(outputMatcher.lineMatcher); + if (match) { + return match; + } + } + } + } + return undefined; +} + +function getXtermLineContent(buffer: IBuffer, lineStart: number, lineEnd: number, cols: number): string { + // Cap the maximum number of lines generated to prevent potential performance problems. This is + // more of a sanity check as the wrapped line should already be trimmed down at this point. + const maxLineLength = Math.max(2048 / cols * 2); + lineEnd = Math.min(lineEnd, lineStart + maxLineLength); + let content = ''; + for (let i = lineStart; i <= lineEnd; i++) { + // Make sure only 0 to cols are considered as resizing when windows mode is enabled will + // retain buffer data outside of the terminal width as reflow is disabled. + const line = buffer.getLine(i); + if (line) { + content += line.translateToString(true, 0, cols); + } + } + return content; +} diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index 9f78d7db571..d45df31b65a 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -331,6 +331,7 @@ export interface IPtyService extends IPtyHostController { reduceConnectionGraceTime(): Promise; requestDetachInstance(workspaceId: string, instanceId: number): Promise; acceptDetachInstanceReply(requestId: number, persistentProcessId?: number): Promise; + freePortKillProcess?(id: number, port: string): Promise<{ port: string; processId: string }>; /** * Serializes and returns terminal state. * @param ids The persistent terminal IDs to serialize. @@ -649,6 +650,11 @@ export interface ITerminalChildProcess { */ detach?(forcePersist?: boolean): Promise; + /** + * Frees the port and kills the process + */ + freePortKillProcess?(port: string): Promise<{ port: string; processId: string }>; + /** * Shutdown the terminal process. * diff --git a/src/vs/platform/terminal/node/ptyHostService.ts b/src/vs/platform/terminal/node/ptyHostService.ts index aab32f8cfcd..41cb6ae3a14 100644 --- a/src/vs/platform/terminal/node/ptyHostService.ts +++ b/src/vs/platform/terminal/node/ptyHostService.ts @@ -316,6 +316,13 @@ export class PtyHostService extends Disposable implements IPtyService { return this._proxy.acceptDetachInstanceReply(requestId, persistentProcessId); } + async freePortKillProcess(id: number, port: string): Promise<{ port: string; processId: string }> { + if (!this._proxy.freePortKillProcess) { + throw new Error('freePortKillProcess does not exist on the pty proxy'); + } + return this._proxy.freePortKillProcess(id, port); + } + async serializeTerminalState(ids: number[]): Promise { return this._proxy.serializeTerminalState(ids); } diff --git a/src/vs/platform/terminal/node/ptyService.ts b/src/vs/platform/terminal/node/ptyService.ts index 0d8f58a6dbd..282bf02ff66 100644 --- a/src/vs/platform/terminal/node/ptyService.ts +++ b/src/vs/platform/terminal/node/ptyService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { execFile } from 'child_process'; +import { execFile, exec } from 'child_process'; import { AutoOpenBarrier, ProcessTimeRunOnceScheduler, Promises, Queue } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -103,6 +103,66 @@ export class PtyService extends Disposable implements IPtyService { this._detachInstanceRequestStore.acceptReply(requestId, processDetails); } + async freePortKillProcess(id: number, port: string): Promise<{ port: string; processId: string }> { + let result: { port: string; processId: string } | undefined; + if (!isWindows) { + const stdout = await new Promise((resolve, reject) => { + exec(`lsof -nP -iTCP -sTCP:LISTEN | grep ${port}`, {}, (err, stdout) => { + if (err) { + return reject('Problem occurred when listing active processes'); + } + resolve(stdout); + }); + }); + const processesForPort = stdout.split('\n'); + if (processesForPort.length >= 1) { + const capturePid = /\s+(\d+)\s+/; + const processId = processesForPort[0].match(capturePid)?.[1]; + if (processId) { + await new Promise((resolve, reject) => { + exec(`kill ${processId}`, {}, (err, stdout) => { + if (err) { + return reject(`Problem occurred when killing the process w ID: ${processId}`); + } + resolve(stdout); + }); + result = { port, processId }; + }); + } + } + } else { + const stdout = await new Promise((resolve, reject) => { + exec(`netstat -ano | findstr "${port}"`, {}, (err, stdout) => { + if (err) { + return reject('Problem occurred when listing active processes'); + } + resolve(stdout); + }); + }); + const processesForPort = stdout.split('\n'); + if (processesForPort.length >= 1) { + const capturePid = /LISTENING\s+(\d{3})/; + const processId = processesForPort[0].match(capturePid)?.[1]; + if (processId) { + await new Promise((resolve, reject) => { + exec(`Taskkill /F /PID ${processId}`, {}, (err, stdout) => { + if (err) { + return reject(`Problem occurred when killing the process w ID: ${processId}`); + } + resolve(stdout); + }); + result = { port, processId }; + }); + } + } + } + + if (result) { + return result; + } + throw new Error(`Processes for port ${port} were not found`); + } + async serializeTerminalState(ids: number[]): Promise { const promises: Promise[] = []; for (const [persistentProcessId, persistentProcess] of this._ptys.entries()) { diff --git a/src/vs/platform/userDataProfile/common/userDataProfile.ts b/src/vs/platform/userDataProfile/common/userDataProfile.ts index 5223d7a9c40..668a16b7c50 100644 --- a/src/vs/platform/userDataProfile/common/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/common/userDataProfile.ts @@ -38,6 +38,7 @@ export interface IUserDataProfile { readonly id: string; readonly isDefault: boolean; readonly name: string; + readonly shortName?: string; readonly location: URI; readonly globalStorageHome: URI; readonly settingsResource: URI; @@ -83,6 +84,16 @@ export type WillRemoveProfileEvent = { join(promise: Promise): void; }; +export interface IUserDataProfileOptions { + readonly shortName?: string; + readonly useDefaultFlags?: UseDefaultProfileFlags; + readonly transient?: boolean; +} + +export interface IUserDataProfileUpdateOptions extends IUserDataProfileOptions { + readonly name?: string; +} + export const IUserDataProfilesService = createDecorator('IUserDataProfilesService'); export interface IUserDataProfilesService { readonly _serviceBrand: undefined; @@ -95,10 +106,10 @@ export interface IUserDataProfilesService { readonly onDidResetWorkspaces: Event; - createNamedProfile(name: string, useDefaultFlags?: UseDefaultProfileFlags, workspaceIdentifier?: WorkspaceIdentifier): Promise; + createNamedProfile(name: string, options?: IUserDataProfileOptions, workspaceIdentifier?: WorkspaceIdentifier): Promise; createTransientProfile(workspaceIdentifier?: WorkspaceIdentifier): Promise; - createProfile(id: string, name: string, useDefaultFlags?: UseDefaultProfileFlags, transient?: boolean, workspaceIdentifier?: WorkspaceIdentifier): Promise; - updateProfile(profile: IUserDataProfile, name: string, useDefaultFlags?: UseDefaultProfileFlags, transient?: boolean): Promise; + createProfile(id: string, name: string, options?: IUserDataProfileOptions, workspaceIdentifier?: WorkspaceIdentifier): Promise; + updateProfile(profile: IUserDataProfile, options?: IUserDataProfileUpdateOptions,): Promise; removeProfile(profile: IUserDataProfile): Promise; setProfileForWorkspace(workspaceIdentifier: WorkspaceIdentifier, profile: IUserDataProfile): Promise; @@ -113,6 +124,7 @@ export function reviveProfile(profile: UriDto, scheme: string) id: profile.id, isDefault: profile.isDefault, name: profile.name, + shortName: profile.shortName, location: URI.revive(profile.location).with({ scheme }), globalStorageHome: URI.revive(profile.globalStorageHome).with({ scheme }), settingsResource: URI.revive(profile.settingsResource).with({ scheme }), @@ -127,20 +139,21 @@ export function reviveProfile(profile: UriDto, scheme: string) export const EXTENSIONS_RESOURCE_NAME = 'extensions.json'; -export function toUserDataProfile(id: string, name: string, location: URI, useDefaultFlags?: UseDefaultProfileFlags, transient?: boolean): IUserDataProfile { +export function toUserDataProfile(id: string, name: string, location: URI, options?: IUserDataProfileOptions): IUserDataProfile { return { id, name, - location: location, + location, isDefault: false, + shortName: options?.shortName, globalStorageHome: joinPath(location, 'globalStorage'), settingsResource: joinPath(location, 'settings.json'), keybindingsResource: joinPath(location, 'keybindings.json'), tasksResource: joinPath(location, 'tasks.json'), snippetsHome: joinPath(location, 'snippets'), extensionsResource: joinPath(location, EXTENSIONS_RESOURCE_NAME), - useDefaultFlags, - isTransient: transient + useDefaultFlags: options?.useDefaultFlags, + isTransient: options?.transient }; } @@ -153,6 +166,7 @@ export type UserDataProfilesObject = { export type StoredUserDataProfile = { name: string; location: URI; + shortName?: string; useDefaultFlags?: UseDefaultProfileFlags; }; @@ -214,7 +228,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf protected _profilesObject: UserDataProfilesObject | undefined; protected get profilesObject(): UserDataProfilesObject { if (!this._profilesObject) { - const profiles = this.enabled ? this.getStoredProfiles().map(storedProfile => toUserDataProfile(basename(storedProfile.location), storedProfile.name, storedProfile.location, storedProfile.useDefaultFlags)) : []; + const profiles = this.enabled ? this.getStoredProfiles().map(storedProfile => toUserDataProfile(basename(storedProfile.location), storedProfile.name, storedProfile.location, { shortName: storedProfile.shortName, useDefaultFlags: storedProfile.useDefaultFlags })) : []; let emptyWindow: IUserDataProfile | undefined; const workspaces = new ResourceMap(); const defaultProfile = toUserDataProfile(hash(this.environmentService.userRoamingDataHome.path).toString(16), localize('defaultProfile', "Default"), this.environmentService.userRoamingDataHome); @@ -251,19 +265,19 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf nameIndex = index > nameIndex ? index : nameIndex; } const name = `${namePrefix} ${nameIndex + 1}`; - return this.createProfile(hash(generateUuid()).toString(16), name, undefined, true, workspaceIdentifier); + return this.createProfile(hash(generateUuid()).toString(16), name, { transient: true }, workspaceIdentifier); } - async createNamedProfile(name: string, useDefaultFlags?: UseDefaultProfileFlags, workspaceIdentifier?: WorkspaceIdentifier): Promise { - return this.createProfile(hash(generateUuid()).toString(16), name, useDefaultFlags, false, workspaceIdentifier); + async createNamedProfile(name: string, options?: IUserDataProfileOptions, workspaceIdentifier?: WorkspaceIdentifier): Promise { + return this.createProfile(hash(generateUuid()).toString(16), name, options, workspaceIdentifier); } - async createProfile(id: string, name: string, useDefaultFlags?: UseDefaultProfileFlags, transient?: boolean, workspaceIdentifier?: WorkspaceIdentifier): Promise { + async createProfile(id: string, name: string, options?: IUserDataProfileOptions, workspaceIdentifier?: WorkspaceIdentifier): Promise { if (!this.enabled) { throw new Error(`Settings Profiles are disabled. Enable them via the '${PROFILES_ENABLEMENT_CONFIG}' setting.`); } - const profile = await this.doCreateProfile(id, name, useDefaultFlags, !!transient); + const profile = await this.doCreateProfile(id, name, options); if (workspaceIdentifier) { await this.setProfileForWorkspace(workspaceIdentifier, profile); @@ -272,7 +286,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf return profile; } - private async doCreateProfile(id: string, name: string, useDefaultFlags: UseDefaultProfileFlags | undefined, transient: boolean): Promise { + private async doCreateProfile(id: string, name: string, options?: IUserDataProfileOptions): Promise { let profileCreationPromise = this.profileCreationPromises.get(name); if (!profileCreationPromise) { profileCreationPromise = (async () => { @@ -282,7 +296,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf return existing; } - const profile = toUserDataProfile(id, name, joinPath(this.profilesHome, id), useDefaultFlags, transient); + const profile = toUserDataProfile(id, name, joinPath(this.profilesHome, id), options); await this.fileService.createFolder(profile.location); const joiners: Promise[] = []; @@ -305,7 +319,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf return profileCreationPromise; } - async updateProfile(profileToUpdate: IUserDataProfile, name: string, useDefaultFlags?: UseDefaultProfileFlags, transient?: boolean): Promise { + async updateProfile(profileToUpdate: IUserDataProfile, options: IUserDataProfileUpdateOptions): Promise { if (!this.enabled) { throw new Error(`Settings Profiles are disabled. Enable them via the '${PROFILES_ENABLEMENT_CONFIG}' setting.`); } @@ -315,7 +329,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf throw new Error(`Profile '${profileToUpdate.name}' does not exist`); } - profile = toUserDataProfile(profile.id, name, profile.location, useDefaultFlags, transient ?? profile.isTransient); + profile = toUserDataProfile(profile.id, options.name ?? profile.name, profile.location, { shortName: options.shortName ?? profile.shortName, transient: options.transient ?? profile.isTransient, useDefaultFlags: options.useDefaultFlags ?? profile.useDefaultFlags }); this.updateProfiles([], [], [profile]); return profile; @@ -482,7 +496,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf if (profile.isTransient) { this.transientProfilesObject.profiles.push(profile); } else { - storedProfiles.push({ location: profile.location, name: profile.name, useDefaultFlags: profile.useDefaultFlags }); + storedProfiles.push({ location: profile.location, name: profile.name, shortName: profile.shortName, useDefaultFlags: profile.useDefaultFlags }); } } this.saveStoredProfiles(storedProfiles); diff --git a/src/vs/platform/userDataProfile/electron-sandbox/userDataProfile.ts b/src/vs/platform/userDataProfile/electron-sandbox/userDataProfile.ts index dda0d6fa9b5..af23d7e7f30 100644 --- a/src/vs/platform/userDataProfile/electron-sandbox/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/electron-sandbox/userDataProfile.ts @@ -10,7 +10,7 @@ import { URI, UriDto } from 'vs/base/common/uri'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services'; -import { DidChangeProfilesEvent, IUserDataProfile, IUserDataProfilesService, reviveProfile, UseDefaultProfileFlags, WorkspaceIdentifier } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { DidChangeProfilesEvent, IUserDataProfile, IUserDataProfileOptions, IUserDataProfilesService, IUserDataProfileUpdateOptions, reviveProfile, WorkspaceIdentifier } from 'vs/platform/userDataProfile/common/userDataProfile'; export class UserDataProfilesNativeService extends Disposable implements IUserDataProfilesService { @@ -48,13 +48,13 @@ export class UserDataProfilesNativeService extends Disposable implements IUserDa this.onDidResetWorkspaces = this.channel.listen('onDidResetWorkspaces'); } - async createNamedProfile(name: string, useDefaultFlags?: UseDefaultProfileFlags, workspaceIdentifier?: WorkspaceIdentifier): Promise { - const result = await this.channel.call>('createNamedProfile', [name, useDefaultFlags, workspaceIdentifier]); + async createNamedProfile(name: string, options?: IUserDataProfileOptions, workspaceIdentifier?: WorkspaceIdentifier): Promise { + const result = await this.channel.call>('createNamedProfile', [name, options, workspaceIdentifier]); return reviveProfile(result, this.profilesHome.scheme); } - async createProfile(id: string, name: string, useDefaultFlags?: UseDefaultProfileFlags, transient?: boolean, workspaceIdentifier?: WorkspaceIdentifier): Promise { - const result = await this.channel.call>('createProfile', [id, name, useDefaultFlags, transient, workspaceIdentifier]); + async createProfile(id: string, name: string, options?: IUserDataProfileOptions, workspaceIdentifier?: WorkspaceIdentifier): Promise { + const result = await this.channel.call>('createProfile', [id, name, options, workspaceIdentifier]); return reviveProfile(result, this.profilesHome.scheme); } @@ -71,8 +71,8 @@ export class UserDataProfilesNativeService extends Disposable implements IUserDa return this.channel.call('removeProfile', [profile]); } - async updateProfile(profile: IUserDataProfile, name: string, useDefaultFlags?: UseDefaultProfileFlags, transient?: boolean): Promise { - const result = await this.channel.call>('updateProfile', [profile, name, useDefaultFlags, transient]); + async updateProfile(profile: IUserDataProfile, updateOptions: IUserDataProfileUpdateOptions): Promise { + const result = await this.channel.call>('updateProfile', [profile, updateOptions]); return reviveProfile(result, this.profilesHome.scheme); } diff --git a/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts b/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts index 3e6d027eead..80475cfdc10 100644 --- a/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts +++ b/src/vs/platform/userDataProfile/test/common/userDataProfileService.test.ts @@ -74,7 +74,7 @@ suite('UserDataProfileService (Common)', () => { }); test('create profile with id, name and transient', async () => { - const profile = await testObject.createProfile('id', 'name', undefined, true); + const profile = await testObject.createProfile('id', 'name', { transient: true }); assert.deepStrictEqual(testObject.profiles.length, 2); assert.deepStrictEqual(profile.id, 'id'); assert.deepStrictEqual(profile.name, 'name'); @@ -86,7 +86,7 @@ suite('UserDataProfileService (Common)', () => { const profile1 = await testObject.createTransientProfile(); const profile2 = await testObject.createTransientProfile(); const profile3 = await testObject.createTransientProfile(); - const profile4 = await testObject.createProfile('id', 'name', undefined, true); + const profile4 = await testObject.createProfile('id', 'name', { transient: true }); assert.deepStrictEqual(testObject.profiles.length, 5); assert.deepStrictEqual(profile1.name, 'Temp 1'); @@ -130,7 +130,7 @@ suite('UserDataProfileService (Common)', () => { test('update named profile', async () => { const profile = await testObject.createNamedProfile('name'); - await testObject.updateProfile(profile, 'name changed'); + await testObject.updateProfile(profile, { name: 'name changed' }); assert.deepStrictEqual(testObject.profiles.length, 2); assert.deepStrictEqual(testObject.profiles[1].name, 'name changed'); @@ -140,7 +140,7 @@ suite('UserDataProfileService (Common)', () => { test('persist transient profile', async () => { const profile = await testObject.createTransientProfile(); - await testObject.updateProfile(profile, 'saved', undefined, false); + await testObject.updateProfile(profile, { name: 'saved', transient: false }); assert.deepStrictEqual(testObject.profiles.length, 2); assert.deepStrictEqual(testObject.profiles[1].name, 'saved'); @@ -149,8 +149,8 @@ suite('UserDataProfileService (Common)', () => { }); test('persist transient profile (2)', async () => { - const profile = await testObject.createProfile('id', 'name', undefined, true); - await testObject.updateProfile(profile, 'saved', undefined, false); + const profile = await testObject.createProfile('id', 'name', { transient: true }); + await testObject.updateProfile(profile, { name: 'saved', transient: false }); assert.deepStrictEqual(testObject.profiles.length, 2); assert.deepStrictEqual(testObject.profiles[1].name, 'saved'); @@ -160,7 +160,7 @@ suite('UserDataProfileService (Common)', () => { test('save transient profile', async () => { const profile = await testObject.createTransientProfile(); - await testObject.updateProfile(profile, 'saved'); + await testObject.updateProfile(profile, { name: 'saved' }); assert.deepStrictEqual(testObject.profiles.length, 2); assert.deepStrictEqual(testObject.profiles[1].name, 'saved'); @@ -168,4 +168,17 @@ suite('UserDataProfileService (Common)', () => { assert.deepStrictEqual(testObject.profiles[1].id, profile.id); }); + test('short name', async () => { + const profile = await testObject.createNamedProfile('name', { shortName: 'short' }); + assert.strictEqual(profile.shortName, 'short'); + + await testObject.updateProfile(profile, { shortName: 'short changed' }); + + assert.deepStrictEqual(testObject.profiles.length, 2); + assert.deepStrictEqual(testObject.profiles[1].name, 'name'); + assert.deepStrictEqual(testObject.profiles[1].shortName, 'short changed'); + assert.deepStrictEqual(!!testObject.profiles[1].isTransient, false); + assert.deepStrictEqual(testObject.profiles[1].id, profile.id); + }); + }); diff --git a/src/vs/platform/userDataSync/common/userDataProfilesManifestMerge.ts b/src/vs/platform/userDataSync/common/userDataProfilesManifestMerge.ts index 202b2fef69c..79212c98d02 100644 --- a/src/vs/platform/userDataSync/common/userDataProfilesManifestMerge.ts +++ b/src/vs/platform/userDataSync/common/userDataProfilesManifestMerge.ts @@ -16,6 +16,7 @@ export type IMergeResult = Required; interface IUserDataProfileInfo { readonly id: string; readonly name: string; + readonly shortName?: string; } export function merge(local: IUserDataProfile[], remote: ISyncUserDataProfile[] | null, lastSync: ISyncUserDataProfile[] | null, ignored: string[]): IMergeResult { @@ -119,13 +120,14 @@ function compare(from: IUserDataProfileInfo[] | null, to: IUserDataProfileInfo[] const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1); const updated: string[] = []; - for (const { id, name } of from) { + for (const { id, name, shortName } of from) { if (removed.includes(id)) { continue; } const toProfile = to.find(p => p.id === id); if (!toProfile || toProfile.name !== name + || toProfile.shortName !== shortName ) { updated.push(id); } diff --git a/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts b/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts index 5ba6f19f3a6..a0f0af035d2 100644 --- a/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts +++ b/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts @@ -182,7 +182,7 @@ export class UserDataProfilesManifestSynchroniser extends AbstractSynchroniser i await this.backupLocal(this.stringifyLocalProfiles(this.getLocalUserDataProfiles(), false)); const promises: Promise[] = []; for (const profile of local.added) { - promises.push(this.userDataProfilesService.createProfile(profile.id, profile.name)); + promises.push(this.userDataProfilesService.createProfile(profile.id, profile.name, { shortName: profile.shortName })); } for (const profile of local.removed) { promises.push(this.userDataProfilesService.removeProfile(profile)); @@ -190,7 +190,7 @@ export class UserDataProfilesManifestSynchroniser extends AbstractSynchroniser i for (const profile of local.updated) { const localProfile = this.userDataProfilesService.profiles.find(p => p.id === profile.id); if (localProfile) { - promises.push(this.userDataProfilesService.updateProfile(localProfile, profile.name)); + promises.push(this.userDataProfilesService.updateProfile(localProfile, { name: profile.name, shortName: profile.shortName })); } else { this.logService.info(`${this.syncResourceLogLabel}: Could not find profile with id '${profile.id}' to update.`); } @@ -203,7 +203,7 @@ export class UserDataProfilesManifestSynchroniser extends AbstractSynchroniser i this.logService.trace(`${this.syncResourceLogLabel}: Updating remote profiles...`); for (const profile of remote?.added || []) { const collection = await this.userDataSyncStoreService.createCollection(this.syncHeaders); - remoteProfiles.push({ id: profile.id, name: profile.name, collection }); + remoteProfiles.push({ id: profile.id, name: profile.name, collection, shortName: profile.shortName }); } for (const profile of remote?.removed || []) { remoteProfiles.splice(remoteProfiles.findIndex(({ id }) => profile.id === id), 1); @@ -211,7 +211,7 @@ export class UserDataProfilesManifestSynchroniser extends AbstractSynchroniser i for (const profile of remote?.updated || []) { const profileToBeUpdated = remoteProfiles.find(({ id }) => profile.id === id); if (profileToBeUpdated) { - remoteProfiles.splice(remoteProfiles.indexOf(profileToBeUpdated), 1, { id: profile.id, name: profile.name, collection: profileToBeUpdated.collection }); + remoteProfiles.splice(remoteProfiles.indexOf(profileToBeUpdated), 1, { id: profile.id, name: profile.name, collection: profileToBeUpdated.collection, shortName: profile.shortName }); } } remoteUserData = await this.updateRemoteUserData(this.stringifyRemoteProfiles(remoteProfiles), force ? null : remoteUserData.ref); diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index dc3f361406a..c3afbb70563 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -320,6 +320,7 @@ export interface ISyncUserDataProfile { readonly id: string; readonly collection: string; readonly name: string; + readonly shortName?: string; } export interface ISyncExtension { diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index 92eefc5e7e4..02cd2ff1c7a 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -14,6 +14,7 @@ import { isBoolean, isUndefined, isUndefinedOrNull } from 'vs/base/common/types' import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -91,6 +92,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService, @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @IProductService private readonly productService: IProductService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, ) { super(); this._status = userDataSyncStoreManagementService.userDataSyncStore ? SyncStatus.Idle : SyncStatus.Uninitialized; @@ -393,7 +396,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ return isUndefined(result) ? null : result; } - if (!this.productService.enableSyncingProfiles) { + if (this.environmentService.isBuilt && !(this.productService.enableSyncingProfiles && this.configurationService.getValue('settingsSync.enableSyncingProfiles'))) { return null; } @@ -520,6 +523,7 @@ class ProfileSynchronizer extends Disposable { @IProductService private readonly productService: IProductService, @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, @IConfigurationService private readonly configurationService: IConfigurationService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, ) { super(); if (this._profile.isDefault) { @@ -564,7 +568,7 @@ class ProfileSynchronizer extends Disposable { if (!this._profile.isDefault) { return; } - if (!this.productService.enableSyncingProfiles) { + if (this.environmentService.isBuilt && !(this.productService.enableSyncingProfiles && this.configurationService.getValue('settingsSync.enableSyncingProfiles'))) { this.logService.debug('Skipping profiles sync'); return; } diff --git a/src/vs/platform/userDataSync/test/common/userDataProfilesManifestMerge.test.ts b/src/vs/platform/userDataSync/test/common/userDataProfilesManifestMerge.test.ts index 9df2650b0e7..8f8e65eb38f 100644 --- a/src/vs/platform/userDataSync/test/common/userDataProfilesManifestMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataProfilesManifestMerge.test.ts @@ -83,7 +83,7 @@ suite('UserDataProfilesManifestMerge', () => { const remoteProfiles: ISyncUserDataProfile[] = [ { id: '1', name: '1', collection: '1' }, { id: '2', name: '2', collection: '2' }, - { id: '3', name: 'changed 3', collection: '3' }, + { id: '3', name: '3', collection: '3', shortName: 'short 3' }, { id: '4', name: 'changed remote', collection: '4' }, { id: '5', name: '5', collection: '5' }, { id: '7', name: '7', collection: '7' }, diff --git a/src/vs/platform/userDataSync/test/common/userDataProfilesManifestSync.test.ts b/src/vs/platform/userDataSync/test/common/userDataProfilesManifestSync.test.ts index 2a8df2f81cc..43e2534b810 100644 --- a/src/vs/platform/userDataSync/test/common/userDataProfilesManifestSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataProfilesManifestSync.test.ts @@ -100,7 +100,7 @@ suite('UserDataProfilesManifestSync', () => { assert.deepStrictEqual(testObject.conflicts.conflicts, []); const profiles = getLocalProfiles(testClient); - assert.deepStrictEqual(profiles, [{ id: '1', name: 'name 1' }]); + assert.deepStrictEqual(profiles, [{ id: '1', name: 'name 1', shortName: undefined }]); }); test('first time sync when profiles exists', async () => { @@ -113,7 +113,7 @@ suite('UserDataProfilesManifestSync', () => { assert.deepStrictEqual(testObject.conflicts.conflicts, []); const profiles = getLocalProfiles(testClient); - assert.deepStrictEqual(profiles, [{ id: '1', name: 'name 1' }, { id: '2', name: 'name 2' }]); + assert.deepStrictEqual(profiles, [{ id: '1', name: 'name 1', shortName: undefined }, { id: '2', name: 'name 2', shortName: undefined }]); const { content } = await testClient.read(testObject.resource); assert.ok(content !== null); @@ -132,7 +132,7 @@ suite('UserDataProfilesManifestSync', () => { assert.deepStrictEqual(testObject.conflicts.conflicts, []); const profiles = getLocalProfiles(testClient); - assert.deepStrictEqual(profiles, [{ id: '1', name: 'name 1' }]); + assert.deepStrictEqual(profiles, [{ id: '1', name: 'name 1', shortName: undefined }]); const { content } = await testClient.read(testObject.resource); assert.ok(content !== null); @@ -141,7 +141,7 @@ suite('UserDataProfilesManifestSync', () => { }); test('sync adding a profile', async () => { - await testClient.instantiationService.get(IUserDataProfilesService).createProfile('1', 'name 1'); + await testClient.instantiationService.get(IUserDataProfilesService).createProfile('1', 'name 1', { shortName: 'short 1' }); await testObject.sync(await testClient.getResourceManifest()); await client2.sync(); @@ -149,15 +149,15 @@ suite('UserDataProfilesManifestSync', () => { await testObject.sync(await testClient.getResourceManifest()); assert.strictEqual(testObject.status, SyncStatus.Idle); assert.deepStrictEqual(testObject.conflicts.conflicts, []); - assert.deepStrictEqual(getLocalProfiles(testClient), [{ id: '1', name: 'name 1' }, { id: '2', name: 'name 2' }]); + assert.deepStrictEqual(getLocalProfiles(testClient), [{ id: '1', name: 'name 1', shortName: 'short 1' }, { id: '2', name: 'name 2', shortName: undefined }]); await client2.sync(); - assert.deepStrictEqual(getLocalProfiles(client2), [{ id: '1', name: 'name 1' }, { id: '2', name: 'name 2' }]); + assert.deepStrictEqual(getLocalProfiles(client2), [{ id: '1', name: 'name 1', shortName: 'short 1' }, { id: '2', name: 'name 2', shortName: undefined }]); const { content } = await testClient.read(testObject.resource); assert.ok(content !== null); const actual = parseRemoteProfiles(content!); - assert.deepStrictEqual(actual, [{ id: '1', name: 'name 1', collection: '1' }, { id: '2', name: 'name 2', collection: '2' }]); + assert.deepStrictEqual(actual, [{ id: '1', name: 'name 1', collection: '1', shortName: 'short 1' }, { id: '2', name: 'name 2', collection: '2' }]); }); test('sync updating a profile', async () => { @@ -165,19 +165,19 @@ suite('UserDataProfilesManifestSync', () => { await testObject.sync(await testClient.getResourceManifest()); await client2.sync(); - await testClient.instantiationService.get(IUserDataProfilesService).updateProfile(profile, 'name 2'); + await testClient.instantiationService.get(IUserDataProfilesService).updateProfile(profile, { name: 'name 2', shortName: '2' }); await testObject.sync(await testClient.getResourceManifest()); assert.strictEqual(testObject.status, SyncStatus.Idle); assert.deepStrictEqual(testObject.conflicts.conflicts, []); - assert.deepStrictEqual(getLocalProfiles(testClient), [{ id: '1', name: 'name 2' }]); + assert.deepStrictEqual(getLocalProfiles(testClient), [{ id: '1', name: 'name 2', shortName: '2' }]); await client2.sync(); - assert.deepStrictEqual(getLocalProfiles(client2), [{ id: '1', name: 'name 2' }]); + assert.deepStrictEqual(getLocalProfiles(client2), [{ id: '1', name: 'name 2', shortName: '2' }]); const { content } = await testClient.read(testObject.resource); assert.ok(content !== null); const actual = parseRemoteProfiles(content!); - assert.deepStrictEqual(actual, [{ id: '1', name: 'name 2', collection: '1' }]); + assert.deepStrictEqual(actual, [{ id: '1', name: 'name 2', collection: '1', shortName: '2' }]); }); test('sync removing a profile', async () => { @@ -190,10 +190,10 @@ suite('UserDataProfilesManifestSync', () => { await testObject.sync(await testClient.getResourceManifest()); assert.strictEqual(testObject.status, SyncStatus.Idle); assert.deepStrictEqual(testObject.conflicts.conflicts, []); - assert.deepStrictEqual(getLocalProfiles(testClient), [{ id: '2', name: 'name 2' }]); + assert.deepStrictEqual(getLocalProfiles(testClient), [{ id: '2', name: 'name 2', shortName: undefined }]); await client2.sync(); - assert.deepStrictEqual(getLocalProfiles(client2), [{ id: '2', name: 'name 2' }]); + assert.deepStrictEqual(getLocalProfiles(client2), [{ id: '2', name: 'name 2', shortName: undefined }]); const { content } = await testClient.read(testObject.resource); assert.ok(content !== null); @@ -206,10 +206,10 @@ suite('UserDataProfilesManifestSync', () => { return JSON.parse(syncData.content); } - function getLocalProfiles(client: UserDataSyncClient): { id: string; name: string }[] { + function getLocalProfiles(client: UserDataSyncClient): { id: string; name: string; shortName?: string }[] { return client.instantiationService.get(IUserDataProfilesService).profiles .slice(1).sort((a, b) => a.name.localeCompare(b.name)) - .map(profile => ({ id: profile.id, name: profile.name })); + .map(profile => ({ id: profile.id, name: profile.name, shortName: profile.shortName })); } diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index 80f11fec953..a0638ba7618 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -77,8 +77,7 @@ export class UserDataSyncClient extends Disposable { insidersUrl: this.testServer.url, canSwitch: false, authenticationProviders: { 'test': { scopes: [] } } - }, - enableSyncingProfiles: true + } } }); diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index ac0a3ab4474..c7ce131f8b1 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -877,7 +877,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { this.readyState = ReadyState.NAVIGATING; // Load URL - this._win.loadURL(FileAccess.asBrowserUri('vs/code/electron-sandbox/workbench/workbench.html', require).toString(true)); + this._win.loadURL(FileAccess.asBrowserUri(`vs/code/electron-sandbox/workbench/workbench${this.environmentMainService.isBuilt ? '' : '-dev'}.html`, require).toString(true)); // Remember that we did load const wasLoaded = this.wasLoaded; diff --git a/src/vs/server/node/remoteTerminalChannel.ts b/src/vs/server/node/remoteTerminalChannel.ts index 7c8f29b1e38..457e31a4451 100644 --- a/src/vs/server/node/remoteTerminalChannel.ts +++ b/src/vs/server/node/remoteTerminalChannel.ts @@ -147,6 +147,7 @@ export class RemoteTerminalChannel extends Disposable implements IServerChannel< case '$refreshProperty': return this._ptyService.refreshProperty.apply(this._ptyService, args); case '$requestDetachInstance': return this._ptyService.requestDetachInstance(args[0], args[1]); case '$acceptDetachedInstance': return this._ptyService.acceptDetachInstanceReply(args[0], args[1]); + case '$freePortKillProcess': return this._ptyService.freePortKillProcess?.apply(args[0], args[1]); } throw new Error(`IPC Command ${command} not found`); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index d91f2831cd6..413b1ff5505 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1171,12 +1171,12 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // namespace: l10n const l10n: typeof vscode.l10n = { - t(...params: [message: string, ...args: string[]] | [{ message: string; args: string[]; comment: string[] }]): string { + t(...params: [message: string, ...args: any[]] | [{ message: string; args?: any[]; comment: string[] }]): string { checkProposedApiEnabled(extension, 'localization'); if (typeof params[0] === 'string') { const key = params.shift() as string; - return extHostLocalization.getMessage(extension.identifier.value, { message: key, args: params as string[] }); + return extHostLocalization.getMessage(extension.identifier.value, { message: key, args: params as any[] | undefined }); } return extHostLocalization.getMessage(extension.identifier.value, params[0]); @@ -1356,6 +1356,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I NotebookCellOutputItem: extHostTypes.NotebookCellOutputItem, NotebookCellStatusBarItem: extHostTypes.NotebookCellStatusBarItem, NotebookControllerAffinity: extHostTypes.NotebookControllerAffinity, + NotebookControllerAffinity2: extHostTypes.NotebookControllerAffinity2, NotebookEdit: extHostTypes.NotebookEdit, PortAttributes: extHostTypes.PortAttributes, LinkedEditingRanges: extHostTypes.LinkedEditingRanges, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 4d64d0b672f..5e21c84cd15 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -2206,7 +2206,7 @@ export interface ExtHostLocalizationShape { export interface IStringDetails { message: string; - args?: string[]; + args?: any[]; comment?: string[]; } diff --git a/src/vs/workbench/api/common/extHostLocalizationService.ts b/src/vs/workbench/api/common/extHostLocalizationService.ts index c09eccb0077..51006adaaaa 100644 --- a/src/vs/workbench/api/common/extHostLocalizationService.ts +++ b/src/vs/workbench/api/common/extHostLocalizationService.ts @@ -54,7 +54,7 @@ export class ExtHostLocalizationService implements ExtHostLocalizationShape { async initializeLocalizedMessages(extension: IExtensionDescription): Promise { if (Language.isDefault() // TODO: support builtin extensions - || !extension.l10nBundleLocation + || !extension.l10n ) { return; } @@ -93,10 +93,9 @@ export class ExtHostLocalizationService implements ExtHostLocalizationShape { // return URI.joinPath(this.initData.nlsBaseUrl, extension.identifier.value, 'main'); // } - if (extension.l10nBundleLocation) { - return URI.joinPath(extension.extensionLocation, extension.l10nBundleLocation); - } - return undefined; + return extension.l10n + ? URI.joinPath(extension.extensionLocation, extension.l10n) + : undefined; } } diff --git a/src/vs/workbench/api/common/extHostNotebookKernels.ts b/src/vs/workbench/api/common/extHostNotebookKernels.ts index cd0ab1203ed..b113127689d 100644 --- a/src/vs/workbench/api/common/extHostNotebookKernels.ts +++ b/src/vs/workbench/api/common/extHostNotebookKernels.ts @@ -18,7 +18,7 @@ import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitData import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; import { ExtHostCell } from 'vs/workbench/api/common/extHostNotebookDocument'; import * as extHostTypeConverters from 'vs/workbench/api/common/extHostTypeConverters'; -import { NotebookCellExecutionState as ExtHostNotebookCellExecutionState, NotebookCellOutput } from 'vs/workbench/api/common/extHostTypes'; +import { NotebookCellExecutionState as ExtHostNotebookCellExecutionState, NotebookCellOutput, NotebookControllerAffinity2 } from 'vs/workbench/api/common/extHostTypes'; import { asWebviewUri } from 'vs/workbench/common/webview'; import { NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellExecutionUpdateType } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; @@ -233,6 +233,11 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { }, // --- priority updateNotebookAffinity(notebook, priority) { + if (priority === NotebookControllerAffinity2.Hidden) { + // This api only adds an extra enum value, the function is the same, so just gate on the new value being passed + // for proposedAPI check. + checkProposedApiEnabled(extension, 'notebookControllerAffinityHidden'); + } that._proxy.$updateNotebookPriority(handle, notebook.uri, priority); }, // --- ipc diff --git a/src/vs/workbench/api/common/extHostOutput.ts b/src/vs/workbench/api/common/extHostOutput.ts index 1b6f01999be..abe63cff8da 100644 --- a/src/vs/workbench/api/common/extHostOutput.ts +++ b/src/vs/workbench/api/common/extHostOutput.ts @@ -17,8 +17,11 @@ import { IExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSyste import { toLocalISOString } from 'vs/base/common/date'; import { VSBuffer } from 'vs/base/common/buffer'; import { isString } from 'vs/base/common/types'; +import { FileSystemProviderErrorCode, toFileSystemProviderErrorCode } from 'vs/platform/files/common/files'; -export class ExtHostLogOutputChannel extends AbstractMessageLogger implements vscode.LogOutputChannel { +class ExtHostOutputChannel extends AbstractMessageLogger implements vscode.LogOutputChannel { + + private offset: number = 0; private _disposed: boolean = false; get disposed(): boolean { return this._disposed; } @@ -35,51 +38,11 @@ export class ExtHostLogOutputChannel extends AbstractMessageLogger implements vs } appendLine(value: string): void { - this.info(value); - } - - show(preserveFocus?: boolean): void { - this.proxy.$reveal(this.id, !!preserveFocus); - } - - hide(): void { - this.proxy.$close(this.id); - } - - protected log(level: LogLevel, message: string): void { - log(this.logger, level, message); - } - - override dispose(): void { - super.dispose(); - - if (!this._disposed) { - this.proxy.$dispose(this.id); - this._disposed = true; - } - } - -} - -export class ExtHostOutputChannel extends ExtHostLogOutputChannel implements vscode.OutputChannel { - - private offset: number = 0; - - constructor( - id: string, name: string, - logger: ILogger, - proxy: MainThreadOutputServiceShape, - extension: IExtensionDescription - ) { - super(id, name, logger, proxy, extension); - } - - override appendLine(value: string): void { this.append(value + '\n'); } append(value: string): void { - this.write(value); + this.info(value); if (this.visible) { this.logger.flush(); this.proxy.$update(this.id, OutputChannelUpdateMode.Append); @@ -94,21 +57,42 @@ export class ExtHostOutputChannel extends ExtHostLogOutputChannel implements vsc replace(value: string): void { const till = this.offset; - this.write(value); + this.info(value); this.proxy.$update(this.id, OutputChannelUpdateMode.Replace, till); if (this.visible) { this.logger.flush(); } } - override show(columnOrPreserveFocus?: vscode.ViewColumn | boolean, preserveFocus?: boolean): void { + show(columnOrPreserveFocus?: vscode.ViewColumn | boolean, preserveFocus?: boolean): void { this.logger.flush(); - super.show(!!(typeof columnOrPreserveFocus === 'boolean' ? columnOrPreserveFocus : preserveFocus)); + this.proxy.$reveal(this.id, !!(typeof columnOrPreserveFocus === 'boolean' ? columnOrPreserveFocus : preserveFocus)); } - private write(value: string): void { - this.offset += VSBuffer.fromString(value).byteLength; - this.logger.info(value); + hide(): void { + this.proxy.$close(this.id); + } + + protected log(level: LogLevel, message: string): void { + this.offset += VSBuffer.fromString(message).byteLength; + log(this.logger, level, message); + } + + override dispose(): void { + super.dispose(); + + if (!this._disposed) { + this.proxy.$dispose(this.id); + this._disposed = true; + } + } + +} + +class ExtHostLogOutputChannel extends ExtHostOutputChannel { + + override appendLine(value: string): void { + this.append(value); } } @@ -121,6 +105,7 @@ export class ExtHostOutputService implements ExtHostOutputServiceShape { private readonly outputsLocation: URI; private outputDirectoryPromise: Thenable | undefined; + private readonly extensionLogDirectoryPromise = new Map>(); private namePool: number = 1; private readonly channels = new Map(); @@ -128,7 +113,7 @@ export class ExtHostOutputService implements ExtHostOutputServiceShape { constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService, - @IExtHostInitDataService initData: IExtHostInitDataService, + @IExtHostInitDataService private readonly initData: IExtHostInitDataService, @IExtHostConsumerFileSystem private readonly extHostFileSystem: IExtHostConsumerFileSystem, @IExtHostFileSystemInfo private readonly extHostFileSystemInfo: IExtHostFileSystemInfo, @ILoggerService private readonly loggerService: ILoggerService, @@ -163,25 +148,40 @@ export class ExtHostOutputService implements ExtHostOutputServiceShape { } private async doCreateOutputChannel(name: string, languageId: string | undefined, extension: IExtensionDescription): Promise { - const file = await this.createLogFile(name); + if (!this.outputDirectoryPromise) { + this.outputDirectoryPromise = this.extHostFileSystem.value.createDirectory(this.outputsLocation).then(() => this.outputsLocation); + } + const outputDir = await this.outputDirectoryPromise; + const file = this.extHostFileSystemInfo.extUri.joinPath(outputDir, `${this.namePool++}-${name.replace(/[\\/:\*\?"<>\|]/g, '')}.log`); const logger = this.loggerService.createLogger(file, { always: true, donotRotate: true, donotUseFormatters: true }); const id = await this.proxy.$register(name, file, false, languageId, extension.identifier.value); return new ExtHostOutputChannel(id, name, logger, this.proxy, extension); } private async doCreateLogOutputChannel(name: string, extension: IExtensionDescription): Promise { - const file = await this.createLogFile(name); + const extensionLogDir = await this.createExtensionLogDirectory(extension); + const file = this.extHostFileSystemInfo.extUri.joinPath(extensionLogDir, `${name.replace(/[\\/:\*\?"<>\|]/g, '')}.log`); const logger = this.loggerService.createLogger(file, { name }); const id = await this.proxy.$register(name, file, true, undefined, extension.identifier.value); return new ExtHostLogOutputChannel(id, name, logger, this.proxy, extension); } - private async createLogFile(name: string): Promise { - if (!this.outputDirectoryPromise) { - this.outputDirectoryPromise = this.extHostFileSystem.value.createDirectory(this.outputsLocation).then(() => this.outputsLocation); + private createExtensionLogDirectory(extension: IExtensionDescription): Thenable { + let extensionLogDirectoryPromise = this.extensionLogDirectoryPromise.get(extension.identifier.value); + if (!extensionLogDirectoryPromise) { + const extensionLogDirectory = this.extHostFileSystemInfo.extUri.joinPath(this.initData.logsLocation, extension.identifier.value); + this.extensionLogDirectoryPromise.set(extension.identifier.value, extensionLogDirectoryPromise = (async () => { + try { + await this.extHostFileSystem.value.createDirectory(extensionLogDirectory); + } catch (err) { + if (toFileSystemProviderErrorCode(err) !== FileSystemProviderErrorCode.FileExists) { + throw err; + } + } + return extensionLogDirectory; + })()); } - const outputDir = await this.outputDirectoryPromise; - return this.extHostFileSystemInfo.extUri.joinPath(outputDir, `${this.namePool++}-${name.replace(/[\\/:\*\?"<>\|]/g, '')}.log`); + return extensionLogDirectoryPromise; } private createExtHostOutputChannel(name: string, channelPromise: Promise): vscode.OutputChannel { @@ -232,38 +232,26 @@ export class ExtHostOutputService implements ExtHostOutputServiceShape { } }; return { - get name(): string { return name; }, - appendLine(value: string): void { + ...this.createExtHostOutputChannel(name, channelPromise), + trace(value: string, ...args: any[]): void { validate(); - channelPromise.then(channel => channel.appendLine(value)); + channelPromise.then(channel => channel.info(value, ...args)); }, - trace(value: string): void { + debug(value: string, ...args: any[]): void { validate(); - channelPromise.then(channel => channel.info(value)); + channelPromise.then(channel => channel.debug(value, ...args)); }, - debug(value: string): void { + info(value: string, ...args: any[]): void { validate(); - channelPromise.then(channel => channel.debug(value)); + channelPromise.then(channel => channel.info(value, ...args)); }, - info(value: string): void { + warn(value: string, ...args: any[]): void { validate(); - channelPromise.then(channel => channel.info(value)); + channelPromise.then(channel => channel.warn(value, ...args)); }, - warn(value: string): void { + error(value: Error | string, ...args: any[]): void { validate(); - channelPromise.then(channel => channel.warn(value)); - }, - error(value: Error | string): void { - validate(); - channelPromise.then(channel => channel.error(value)); - }, - show(preserveFocus?: boolean): void { - validate(); - channelPromise.then(channel => channel.show(preserveFocus)); - }, - hide(): void { - validate(); - channelPromise.then(channel => channel.hide()); + channelPromise.then(channel => channel.error(value, ...args)); }, dispose(): void { disposed = true; diff --git a/src/vs/workbench/api/common/extHostTestItem.ts b/src/vs/workbench/api/common/extHostTestItem.ts index d406c5bc870..25fa2780a45 100644 --- a/src/vs/workbench/api/common/extHostTestItem.ts +++ b/src/vs/workbench/api/common/extHostTestItem.ts @@ -68,7 +68,24 @@ const evSetProps = (fn: (newValue: T) => Partial): (newValue: T) = v => ({ op: TestItemEventOp.SetProp, update: fn(v) }); const makePropDescriptors = (api: IExtHostTestItemApi, label: string): { [K in keyof Required]: PropertyDescriptor } => ({ - range: testItemPropAccessor<'range'>(api, undefined, propComparators.range, evSetProps(r => ({ range: editorRange.Range.lift(Convert.Range.from(r)) }))), + range: (() => { + let value: vscode.Range | undefined; + const updateProps = evSetProps(r => ({ range: editorRange.Range.lift(Convert.Range.from(r)) })); + return { + enumerable: true, + configurable: false, + get() { + return value; + }, + set(newValue: vscode.Range | undefined) { + api.listener?.({ op: TestItemEventOp.DocumentSynced }); + if (!propComparators.range(value, newValue)) { + value = newValue; + api.listener?.(updateProps(newValue)); + } + }, + }; + })(), label: testItemPropAccessor<'label'>(api, label, propComparators.label, evSetProps(label => ({ label }))), description: testItemPropAccessor<'description'>(api, undefined, propComparators.description, evSetProps(description => ({ description }))), sortText: testItemPropAccessor<'sortText'>(api, undefined, propComparators.sortText, evSetProps(sortText => ({ sortText }))), diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 926fde45ea1..57c2608bdfc 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3592,6 +3592,12 @@ export enum NotebookControllerAffinity { Preferred = 2 } +export enum NotebookControllerAffinity2 { + Default = 1, + Preferred = 2, + Hidden = -1 +} + export class NotebookRendererScript { public provides: readonly string[]; diff --git a/src/vs/workbench/api/test/browser/extHostTesting.test.ts b/src/vs/workbench/api/test/browser/extHostTesting.test.ts index 2cdcad84cb5..dad5ae26397 100644 --- a/src/vs/workbench/api/test/browser/extHostTesting.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTesting.test.ts @@ -18,6 +18,7 @@ import { TestDiffOpType, TestItemExpandState, TestMessageType, TestsDiff } from import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import type { TestItem, TestRunRequest } from 'vscode'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import * as editorRange from 'vs/editor/common/core/range'; const simplify = (item: TestItem) => ({ id: item.id, @@ -153,7 +154,7 @@ suite('ExtHost Testing', () => { assert.deepStrictEqual(single.collectDiff(), [ { op: TestDiffOpType.Update, - item: { extId: new TestId(['ctrlId', 'id-a']).toString(), docv: undefined, item: { description: 'Hello world' } }, + item: { extId: new TestId(['ctrlId', 'id-a']).toString(), item: { description: 'Hello world' } }, } ]); }); @@ -254,7 +255,7 @@ suite('ExtHost Testing', () => { assert.deepStrictEqual(single.collectDiff(), [ { op: TestDiffOpType.Update, - item: { extId: new TestId(['ctrlId', 'id-a']).toString(), expand: TestItemExpandState.Expanded, docv: undefined, item: { label: 'Hello world' } }, + item: { extId: new TestId(['ctrlId', 'id-a']).toString(), expand: TestItemExpandState.Expanded, item: { label: 'Hello world' } }, }, ]); @@ -262,7 +263,7 @@ suite('ExtHost Testing', () => { assert.deepStrictEqual(single.collectDiff(), [ { op: TestDiffOpType.Update, - item: { extId: new TestId(['ctrlId', 'id-a']).toString(), docv: undefined, item: { label: 'still connected' } } + item: { extId: new TestId(['ctrlId', 'id-a']).toString(), item: { label: 'still connected' } } }, ]); @@ -289,7 +290,7 @@ suite('ExtHost Testing', () => { }, { op: TestDiffOpType.Update, - item: { extId: TestId.fromExtHostTestItem(oldAB, 'ctrlId').toString(), docv: undefined, item: { label: 'Hello world' } }, + item: { extId: TestId.fromExtHostTestItem(oldAB, 'ctrlId').toString(), item: { label: 'Hello world' } }, }, ]); @@ -299,11 +300,11 @@ suite('ExtHost Testing', () => { assert.deepStrictEqual(single.collectDiff(), [ { op: TestDiffOpType.Update, - item: { extId: new TestId(['ctrlId', 'id-a', 'id-aa']).toString(), docv: undefined, item: { label: 'still connected1' } } + item: { extId: new TestId(['ctrlId', 'id-a', 'id-aa']).toString(), item: { label: 'still connected1' } } }, { op: TestDiffOpType.Update, - item: { extId: new TestId(['ctrlId', 'id-a', 'id-ab']).toString(), docv: undefined, item: { label: 'still connected2' } } + item: { extId: new TestId(['ctrlId', 'id-a', 'id-ab']).toString(), item: { label: 'still connected2' } } }, ]); @@ -333,13 +334,65 @@ suite('ExtHost Testing', () => { assert.deepStrictEqual(single.collectDiff(), [ { op: TestDiffOpType.Update, - item: { extId: new TestId(['ctrlId', 'id-a', 'id-b']).toString(), docv: undefined, item: { label: 'still connected' } } + item: { extId: new TestId(['ctrlId', 'id-a', 'id-b']).toString(), item: { label: 'still connected' } } }, ]); assert.deepStrictEqual([...single.root.children].map(([_, item]) => item), [single.root.children.get('id-a')]); assert.deepStrictEqual(b.parent, a); }); + + test('sends document sync events', async () => { + await single.expand(single.root.id, 0); + single.collectDiff(); + + const a = single.root.children.get('id-a') as TestItemImpl; + a.range = new Range(new Position(0, 0), new Position(1, 0)); + + assert.deepStrictEqual(single.collectDiff(), [ + { + op: TestDiffOpType.DocumentSynced, + docv: undefined, + uri: URI.file('/') + }, + { + op: TestDiffOpType.Update, + item: { + extId: new TestId(['ctrlId', 'id-a']).toString(), + item: { + range: editorRange.Range.lift({ + endColumn: 1, + endLineNumber: 2, + startColumn: 1, + startLineNumber: 1 + }) + } + }, + }, + ]); + + // sends on replace even if it's a no-op + a.range = a.range; + assert.deepStrictEqual(single.collectDiff(), [ + { + op: TestDiffOpType.DocumentSynced, + docv: undefined, + uri: URI.file('/') + }, + ]); + + // sends on a child replacement + const a2 = new TestItemImpl('ctrlId', 'id-a', 'a', URI.file('/')); + a2.range = a.range; + single.root.children.replace([a2, single.root.children.get('id-b')!]); + assert.deepStrictEqual(single.collectDiff(), [ + { + op: TestDiffOpType.DocumentSynced, + docv: undefined, + uri: URI.file('/') + }, + ]); + }); }); diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index ec50fb9eff1..ea93a14b908 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -9,13 +9,13 @@ import { EventType, addDisposableListener, getClientArea, Dimension, position, s import { onDidChangeFullscreen, isFullscreen } from 'vs/base/browser/browser'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { isWindows, isLinux, isMacintosh, isWeb, isNative, isIOS } from 'vs/base/common/platform'; -import { EditorInputCapabilities, isResourceEditorInput, IUntypedEditorInput, pathsToEditors } from 'vs/workbench/common/editor'; +import { EditorInputCapabilities, GroupIdentifier, isResourceEditorInput, IUntypedEditorInput, pathsToEditors } from 'vs/workbench/common/editor'; import { SidebarPart } from 'vs/workbench/browser/parts/sidebar/sidebarPart'; import { PanelPart } from 'vs/workbench/browser/parts/panel/panelPart'; import { Position, Parts, PanelOpensMaximizedOptions, IWorkbenchLayoutService, positionFromString, positionToString, panelOpensMaximizedFromString, PanelAlignment } from 'vs/workbench/services/layout/browser/layoutService'; import { isTemporaryWorkspace, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IStorageService, StorageScope, WillSaveStateReason } from 'vs/platform/storage/common/storage'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IStorageService, StorageScope, StorageTarget, WillSaveStateReason } from 'vs/platform/storage/common/storage'; +import { IConfigurationChangeEvent, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITitleService } from 'vs/workbench/services/title/common/titleService'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { StartupKind, ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; @@ -24,7 +24,7 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IEditor } from 'vs/editor/common/editorCommon'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { EditorGroupLayout, GroupsOrder, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { SerializableGrid, ISerializableView, ISerializedGrid, Orientation, ISerializedNode, ISerializedLeafNode, Direction, IViewSize, Sizing } from 'vs/base/browser/ui/grid/grid'; import { Part } from 'vs/workbench/browser/part'; import { IStatusbarService } from 'vs/workbench/services/statusbar/browser/statusbar'; @@ -48,42 +48,51 @@ import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/b import { ActivitybarPart } from 'vs/workbench/browser/parts/activitybar/activitybarPart'; import { AuxiliaryBarPart } from 'vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { LayoutStateKeys, LayoutStateModel, WorkbenchLayoutSettings } from 'vs/workbench/browser/layoutState'; -interface IWorkbenchLayoutWindowRuntimeState { +//#region Layout Implementation + +interface ILayoutRuntimeState { fullscreen: boolean; maximized: boolean; hasFocus: boolean; windowBorder: boolean; - menuBar: { + readonly menuBar: { toggled: boolean; }; - zenMode: { - transitionDisposables: DisposableStore; + readonly zenMode: { + readonly transitionDisposables: DisposableStore; }; } -interface IWorkbenchLayoutWindowInitializationState { - views: { - defaults: string[] | undefined; - containerToRestore: { +interface IEditorToOpen { + readonly editor: IUntypedEditorInput; + readonly viewColumn?: number; +} + +interface ILayoutInitializationState { + readonly views: { + readonly defaults: string[] | undefined; + readonly containerToRestore: { sideBar?: string; panel?: string; auxiliaryBar?: string; }; }; - editor: { - restoreEditors: boolean; - editorsToOpen: Promise; + readonly editor: { + readonly restoreEditors: boolean; + readonly editorsToOpen: Promise; + }; + readonly layout?: { + readonly editors?: EditorGroupLayout; }; } -interface IWorkbenchLayoutWindowState { - runtime: IWorkbenchLayoutWindowRuntimeState; - initialization: IWorkbenchLayoutWindowInitializationState; +interface ILayoutState { + readonly runtime: ILayoutRuntimeState; + readonly initialization: ILayoutInitializationState; } -enum WorkbenchLayoutClasses { +enum LayoutClasses { SIDEBAR_HIDDEN = 'nosidebar', EDITOR_HIDDEN = 'noeditorarea', PANEL_HIDDEN = 'nopanel', @@ -94,10 +103,16 @@ enum WorkbenchLayoutClasses { WINDOW_BORDER = 'border' } -interface IInitialFilesToOpen { - filesToOpenOrCreate?: IPath[]; - filesToDiff?: IPath[]; - filesToMerge?: IPath[]; +interface IPathToOpen extends IPath { + readonly viewColumn?: number; +} + +interface IInitialEditorsState { + readonly filesToOpenOrCreate?: IPathToOpen[]; + readonly filesToDiff?: IPathToOpen[]; + readonly filesToMerge?: IPathToOpen[]; + + readonly layout?: EditorGroupLayout; } export abstract class Layout extends Disposable implements IWorkbenchLayoutService { @@ -187,7 +202,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private logService!: ILogService; private telemetryService!: ITelemetryService; - private windowState!: IWorkbenchLayoutWindowState; + private state!: ILayoutState; private stateModel!: LayoutStateModel; private disposed = false; @@ -279,8 +294,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } private onMenubarToggled(visible: boolean): void { - if (visible !== this.windowState.runtime.menuBar.toggled) { - this.windowState.runtime.menuBar.toggled = visible; + if (visible !== this.state.runtime.menuBar.toggled) { + this.state.runtime.menuBar.toggled = visible; const menuBarVisibility = getMenuBarVisibility(this.configurationService); @@ -290,7 +305,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } // The menu bar toggles the title bar in full screen for toggle and classic settings - else if (this.windowState.runtime.fullscreen && (menuBarVisibility === 'toggle' || menuBarVisibility === 'classic')) { + else if (this.state.runtime.fullscreen && (menuBarVisibility === 'toggle' || menuBarVisibility === 'classic')) { this.workbenchGrid.setViewVisible(this.titleBarPartView, this.shouldShowTitleBar()); } @@ -302,13 +317,13 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } private onFullscreenChanged(): void { - this.windowState.runtime.fullscreen = isFullscreen(); + this.state.runtime.fullscreen = isFullscreen(); // Apply as CSS class - if (this.windowState.runtime.fullscreen) { - this.container.classList.add(WorkbenchLayoutClasses.FULLSCREEN); + if (this.state.runtime.fullscreen) { + this.container.classList.add(LayoutClasses.FULLSCREEN); } else { - this.container.classList.remove(WorkbenchLayoutClasses.FULLSCREEN); + this.container.classList.remove(LayoutClasses.FULLSCREEN); const zenModeExitInfo = this.stateModel.getRuntimeValue(LayoutStateKeys.ZEN_MODE_EXIT_INFO); const zenModeActive = this.stateModel.getRuntimeValue(LayoutStateKeys.ZEN_MODE_ACTIVE); @@ -318,7 +333,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } // Change edge snapping accordingly - this.workbenchGrid.edgeSnapping = this.windowState.runtime.fullscreen; + this.workbenchGrid.edgeSnapping = this.state.runtime.fullscreen; // Changing fullscreen state of the window has an impact // on custom title bar visibility, so we need to update @@ -330,15 +345,15 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.updateWindowBorder(true); } - this._onDidChangeFullscreen.fire(this.windowState.runtime.fullscreen); + this._onDidChangeFullscreen.fire(this.state.runtime.fullscreen); } private onWindowFocusChanged(hasFocus: boolean): void { - if (this.windowState.runtime.hasFocus === hasFocus) { + if (this.state.runtime.hasFocus === hasFocus) { return; } - this.windowState.runtime.hasFocus = hasFocus; + this.state.runtime.hasFocus = hasFocus; this.updateWindowBorder(); } @@ -401,21 +416,21 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi const inactiveBorder = theme.getColor(WINDOW_INACTIVE_BORDER); let windowBorder = false; - if (!this.windowState.runtime.fullscreen && !this.windowState.runtime.maximized && (activeBorder || inactiveBorder)) { + if (!this.state.runtime.fullscreen && !this.state.runtime.maximized && (activeBorder || inactiveBorder)) { windowBorder = true; // If the inactive color is missing, fallback to the active one - const borderColor = this.windowState.runtime.hasFocus ? activeBorder : inactiveBorder ?? activeBorder; + const borderColor = this.state.runtime.hasFocus ? activeBorder : inactiveBorder ?? activeBorder; this.container.style.setProperty('--window-border-color', borderColor?.toString() ?? 'transparent'); } - if (windowBorder === this.windowState.runtime.windowBorder) { + if (windowBorder === this.state.runtime.windowBorder) { return; } - this.windowState.runtime.windowBorder = windowBorder; + this.state.runtime.windowBorder = windowBorder; - this.container.classList.toggle(WorkbenchLayoutClasses.WINDOW_BORDER, windowBorder); + this.container.classList.toggle(LayoutClasses.WINDOW_BORDER, windowBorder); if (!skipLayout) { this.layout(); @@ -459,12 +474,15 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.doUpdateLayoutConfiguration(); }); - // Window Initialization State - const initialFilesToOpen = this.getInitialFilesToOpen(); - const windowInitializationState: IWorkbenchLayoutWindowInitializationState = { + // Layout Initialization State + const initialEditorsState = this.getInitialEditorsState(); + const initialLayoutState: ILayoutInitializationState = { + layout: { + editors: initialEditorsState?.layout + }, editor: { - restoreEditors: this.shouldRestoreEditors(this.contextService, initialFilesToOpen), - editorsToOpen: this.resolveEditorsToOpen(fileService, initialFilesToOpen) + restoreEditors: this.shouldRestoreEditors(this.contextService, initialEditorsState), + editorsToOpen: this.resolveEditorsToOpen(fileService, initialEditorsState), }, views: { defaults: this.getDefaultLayoutViews(this.environmentService, this.storageService), @@ -472,8 +490,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } }; - // Window Runtime State - const windowRuntimeState: IWorkbenchLayoutWindowRuntimeState = { + // Layout Runtime State + const layoutRuntimeState: ILayoutRuntimeState = { fullscreen: isFullscreen(), hasFocus: this.hostService.hasFocus, maximized: false, @@ -486,9 +504,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } }; - this.windowState = { - initialization: windowInitializationState, - runtime: windowRuntimeState, + this.state = { + initialization: initialLayoutState, + runtime: layoutRuntimeState, }; // Sidebar View Container To Restore @@ -503,7 +521,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } if (viewContainerToRestore) { - this.windowState.initialization.views.containerToRestore.sideBar = viewContainerToRestore; + this.state.initialization.views.containerToRestore.sideBar = viewContainerToRestore; } else { this.stateModel.setRuntimeValue(LayoutStateKeys.SIDEBAR_HIDDEN, true); } @@ -514,7 +532,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi const viewContainerToRestore = this.storageService.get(PanelPart.activePanelSettingsKey, StorageScope.WORKSPACE, this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Panel)?.id); if (viewContainerToRestore) { - this.windowState.initialization.views.containerToRestore.panel = viewContainerToRestore; + this.state.initialization.views.containerToRestore.panel = viewContainerToRestore; } else { this.stateModel.setRuntimeValue(LayoutStateKeys.PANEL_HIDDEN, true); } @@ -525,7 +543,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi const viewContainerToRestore = this.storageService.get(AuxiliaryBarPart.activePanelSettingsKey, StorageScope.WORKSPACE, this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.AuxiliaryBar)?.id); if (viewContainerToRestore) { - this.windowState.initialization.views.containerToRestore.auxiliaryBar = viewContainerToRestore; + this.state.initialization.views.containerToRestore.auxiliaryBar = viewContainerToRestore; } else { this.stateModel.setRuntimeValue(LayoutStateKeys.AUXILIARYBAR_HIDDEN, true); } @@ -553,7 +571,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return undefined; } - private shouldRestoreEditors(contextService: IWorkspaceContextService, initialFilesToOpen: IInitialFilesToOpen | undefined): boolean { + private shouldRestoreEditors(contextService: IWorkspaceContextService, initialEditorsState: IInitialEditorsState | undefined): boolean { // Restore editors based on a set of rules: // - never when running on temporary workspace @@ -565,40 +583,56 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } const forceRestoreEditors = this.configurationService.getValue('window.restoreWindows') === 'preserve'; - return !!forceRestoreEditors || initialFilesToOpen === undefined; + return !!forceRestoreEditors || initialEditorsState === undefined; } protected willRestoreEditors(): boolean { - return this.windowState.initialization.editor.restoreEditors; + return this.state.initialization.editor.restoreEditors; } - private async resolveEditorsToOpen(fileService: IFileService, initialFilesToOpen: IInitialFilesToOpen | undefined): Promise { - if (initialFilesToOpen) { + private async resolveEditorsToOpen(fileService: IFileService, initialEditorsState: IInitialEditorsState | undefined): Promise { + if (initialEditorsState) { - // Merge editor - const filesToMerge = await pathsToEditors(initialFilesToOpen.filesToMerge, fileService); + // Merge editor (single) + const filesToMerge = coalesce(await pathsToEditors(initialEditorsState.filesToMerge, fileService)); if (filesToMerge.length === 4 && isResourceEditorInput(filesToMerge[0]) && isResourceEditorInput(filesToMerge[1]) && isResourceEditorInput(filesToMerge[2]) && isResourceEditorInput(filesToMerge[3])) { return [{ - input1: { resource: filesToMerge[0].resource }, - input2: { resource: filesToMerge[1].resource }, - base: { resource: filesToMerge[2].resource }, - result: { resource: filesToMerge[3].resource }, - options: { pinned: true } + editor: { + input1: { resource: filesToMerge[0].resource }, + input2: { resource: filesToMerge[1].resource }, + base: { resource: filesToMerge[2].resource }, + result: { resource: filesToMerge[3].resource }, + options: { pinned: true } + } }]; } - // Diff editor - const filesToDiff = await pathsToEditors(initialFilesToOpen.filesToDiff, fileService); + // Diff editor (single) + const filesToDiff = coalesce(await pathsToEditors(initialEditorsState.filesToDiff, fileService)); if (filesToDiff.length === 2) { return [{ - original: { resource: filesToDiff[0].resource }, - modified: { resource: filesToDiff[1].resource }, - options: { pinned: true } + editor: { + original: { resource: filesToDiff[0].resource }, + modified: { resource: filesToDiff[1].resource }, + options: { pinned: true } + } }]; } - // Normal editor - return pathsToEditors(initialFilesToOpen.filesToOpenOrCreate, fileService); + // Normal editor (multiple) + const filesToOpenOrCreate: IEditorToOpen[] = []; + const resolvedFilesToOpenOrCreate = await pathsToEditors(initialEditorsState.filesToOpenOrCreate, fileService); + for (let i = 0; i < resolvedFilesToOpenOrCreate.length; i++) { + const resolvedFileToOpenOrCreate = resolvedFilesToOpenOrCreate[i]; + if (resolvedFileToOpenOrCreate) { + filesToOpenOrCreate.push({ + editor: resolvedFileToOpenOrCreate, + viewColumn: initialEditorsState.filesToOpenOrCreate?.[i].viewColumn // take over `viewColumn` from initial state + }); + } + } + + return filesToOpenOrCreate; } // Empty workbench configured to open untitled file if empty @@ -612,7 +646,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return []; // do not open any empty untitled file if we have backups to restore } - return [{ resource: undefined }]; // open empty untitled file + return [{ + editor: { resource: undefined } // open empty untitled file + }]; } return []; @@ -621,30 +657,32 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private _openedDefaultEditors: boolean = false; get openedDefaultEditors() { return this._openedDefaultEditors; } - private getInitialFilesToOpen(): IInitialFilesToOpen | undefined { + private getInitialEditorsState(): IInitialEditorsState | undefined { - // Check for editors from `defaultLayout` options first + // Check for editors / editor layout from `defaultLayout` options first const defaultLayout = this.environmentService.options?.defaultLayout; - if (defaultLayout?.editors?.length && (defaultLayout.force || this.storageService.isNew(StorageScope.WORKSPACE))) { + if ((defaultLayout?.editors?.length || defaultLayout?.layout?.editors) && (defaultLayout.force || this.storageService.isNew(StorageScope.WORKSPACE))) { this._openedDefaultEditors = true; return { - filesToOpenOrCreate: defaultLayout.editors.map(file => { - const legacyOverride = file.openWith; - const legacySelection = file.selection && file.selection.start && isNumber(file.selection.start.line) ? { - startLineNumber: file.selection.start.line, - startColumn: isNumber(file.selection.start.column) ? file.selection.start.column : 1, - endLineNumber: isNumber(file.selection.end.line) ? file.selection.end.line : undefined, - endColumn: isNumber(file.selection.end.line) ? (isNumber(file.selection.end.column) ? file.selection.end.column : 1) : undefined, + layout: defaultLayout.layout?.editors, + filesToOpenOrCreate: defaultLayout?.editors?.map(editor => { + // TODO@bpasero remove me eventually + const editor2 = editor as any; + const legacySelection = editor2.selection && editor2.selection.start && isNumber(editor2.selection.start.line) ? { + startLineNumber: editor2.selection.start.line, + startColumn: isNumber(editor2.selection.start.column) ? editor2.selection.start.column : 1, + endLineNumber: isNumber(editor2.selection.end.line) ? editor2.selection.end.line : undefined, + endColumn: isNumber(editor2.selection.end.line) ? (isNumber(editor2.selection.end.column) ? editor2.selection.end.column : 1) : undefined, } : undefined; return { - fileUri: URI.revive(file.uri), - openOnlyIfExists: file.openOnlyIfExists, + viewColumn: editor.viewColumn, + fileUri: URI.revive(editor.uri), + openOnlyIfExists: editor.openOnlyIfExists, options: { selection: legacySelection, - override: legacyOverride, - ...file.options // keep at the end to override legacy selection/override that may be `undefined` + ...editor.options // keep at the end to override legacy selection/override that may be `undefined` } }; }) @@ -686,6 +724,11 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // first ensure the editor part is ready await this.editorGroupService.whenReady; + // apply editor layout if any + if (this.state.initialization.layout?.editors) { + this.editorGroupService.applyLayout(this.state.initialization.layout.editors); + } + // then see for editors to open as instructed // it is important that we trigger this from // the overall restore flow to reduce possible @@ -694,11 +737,34 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // signaling that layout is restored, but we do // not need to await the editors from having // fully loaded. - const editors = await this.windowState.initialization.editor.editorsToOpen; + + const editors = await this.state.initialization.editor.editorsToOpen; let openEditorsPromise: Promise | undefined = undefined; if (editors.length) { - openEditorsPromise = this.editorService.openEditors(editors, undefined, { validateTrust: true }); + + // we have to map editors to their groups as instructed + // by the input. this is important to ensure that we open + // the editors in the groups they belong to. + + const editorGroupsInVisualOrder = this.editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE); + const mapEditorsToGroup = new Map>(); + + for (const editor of editors) { + const group = editorGroupsInVisualOrder[(editor.viewColumn ?? 1) - 1]; // viewColumn is index+1 based + + let editorsByGroup = mapEditorsToGroup.get(group.id); + if (!editorsByGroup) { + editorsByGroup = new Set(); + mapEditorsToGroup.set(group.id, editorsByGroup); + } + + editorsByGroup.add(editor.editor); + } + + openEditorsPromise = Promise.all(Array.from(mapEditorsToGroup).map(async ([groupId, editors]) => { + return this.editorService.openEditors(Array.from(editors), groupId, { validateTrust: true }); + })); } // do not block the overall layout ready flow from potentially @@ -718,7 +784,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Restore default views (only when `IDefaultLayout` is provided) const restoreDefaultViewsPromise = (async () => { - if (this.windowState.initialization.views.defaults?.length) { + if (this.state.initialization.views.defaults?.length) { mark('code/willOpenDefaultViews'); const locationsRestored: { id: string; order: number }[] = []; @@ -743,7 +809,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return false; }; - const defaultViews = [...this.windowState.initialization.views.defaults].reverse().map((v, index) => ({ id: v, order: index })); + const defaultViews = [...this.state.initialization.views.defaults].reverse().map((v, index) => ({ id: v, order: index })); let i = defaultViews.length; while (i) { @@ -768,17 +834,17 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // If we opened a view in the sidebar, stop any restore there if (locationsRestored[ViewContainerLocation.Sidebar]) { - this.windowState.initialization.views.containerToRestore.sideBar = locationsRestored[ViewContainerLocation.Sidebar].id; + this.state.initialization.views.containerToRestore.sideBar = locationsRestored[ViewContainerLocation.Sidebar].id; } // If we opened a view in the panel, stop any restore there if (locationsRestored[ViewContainerLocation.Panel]) { - this.windowState.initialization.views.containerToRestore.panel = locationsRestored[ViewContainerLocation.Panel].id; + this.state.initialization.views.containerToRestore.panel = locationsRestored[ViewContainerLocation.Panel].id; } // If we opened a view in the auxiliary bar, stop any restore there if (locationsRestored[ViewContainerLocation.AuxiliaryBar]) { - this.windowState.initialization.views.containerToRestore.auxiliaryBar = locationsRestored[ViewContainerLocation.AuxiliaryBar].id; + this.state.initialization.views.containerToRestore.auxiliaryBar = locationsRestored[ViewContainerLocation.AuxiliaryBar].id; } mark('code/didOpenDefaultViews'); @@ -792,13 +858,13 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Restoring views could mean that sidebar already // restored, as such we need to test again await restoreDefaultViewsPromise; - if (!this.windowState.initialization.views.containerToRestore.sideBar) { + if (!this.state.initialization.views.containerToRestore.sideBar) { return; } mark('code/willRestoreViewlet'); - const viewlet = await this.paneCompositeService.openPaneComposite(this.windowState.initialization.views.containerToRestore.sideBar, ViewContainerLocation.Sidebar); + const viewlet = await this.paneCompositeService.openPaneComposite(this.state.initialization.views.containerToRestore.sideBar, ViewContainerLocation.Sidebar); if (!viewlet) { await this.paneCompositeService.openPaneComposite(this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Sidebar)?.id, ViewContainerLocation.Sidebar); // fallback to default viewlet as needed } @@ -812,13 +878,13 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Restoring views could mean that panel already // restored, as such we need to test again await restoreDefaultViewsPromise; - if (!this.windowState.initialization.views.containerToRestore.panel) { + if (!this.state.initialization.views.containerToRestore.panel) { return; } mark('code/willRestorePanel'); - const panel = await this.paneCompositeService.openPaneComposite(this.windowState.initialization.views.containerToRestore.panel, ViewContainerLocation.Panel); + const panel = await this.paneCompositeService.openPaneComposite(this.state.initialization.views.containerToRestore.panel, ViewContainerLocation.Panel); if (!panel) { await this.paneCompositeService.openPaneComposite(this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Panel)?.id, ViewContainerLocation.Panel); // fallback to default panel as needed } @@ -832,13 +898,13 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Restoring views could mean that panel already // restored, as such we need to test again await restoreDefaultViewsPromise; - if (!this.windowState.initialization.views.containerToRestore.auxiliaryBar) { + if (!this.state.initialization.views.containerToRestore.auxiliaryBar) { return; } mark('code/willRestoreAuxiliaryBar'); - const panel = await this.paneCompositeService.openPaneComposite(this.windowState.initialization.views.containerToRestore.auxiliaryBar, ViewContainerLocation.AuxiliaryBar); + const panel = await this.paneCompositeService.openPaneComposite(this.state.initialization.views.containerToRestore.auxiliaryBar, ViewContainerLocation.AuxiliaryBar); if (!panel) { await this.paneCompositeService.openPaneComposite(this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.AuxiliaryBar)?.id, ViewContainerLocation.AuxiliaryBar); // fallback to default panel as needed } @@ -987,11 +1053,11 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // macOS desktop does not need a title bar when full screen if (isMacintosh && isNative) { - return !this.windowState.runtime.fullscreen; + return !this.state.runtime.fullscreen; } // non-fullscreen native must show the title bar - if (isNative && !this.windowState.runtime.fullscreen) { + if (isNative && !this.state.runtime.fullscreen) { return true; } @@ -1003,16 +1069,16 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // remaining behavior is based on menubar visibility switch (getMenuBarVisibility(this.configurationService)) { case 'classic': - return !this.windowState.runtime.fullscreen || this.windowState.runtime.menuBar.toggled; + return !this.state.runtime.fullscreen || this.state.runtime.menuBar.toggled; case 'compact': case 'hidden': return false; case 'toggle': - return this.windowState.runtime.menuBar.toggled; + return this.state.runtime.menuBar.toggled; case 'visible': return true; default: - return isWeb ? false : !this.windowState.runtime.fullscreen || this.windowState.runtime.menuBar.toggled; + return isWeb ? false : !this.state.runtime.fullscreen || this.state.runtime.menuBar.toggled; } } @@ -1046,7 +1112,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi toggleZenMode(skipLayout?: boolean, restoring = false): void { this.stateModel.setRuntimeValue(LayoutStateKeys.ZEN_MODE_ACTIVE, !this.stateModel.getRuntimeValue(LayoutStateKeys.ZEN_MODE_ACTIVE)); - this.windowState.runtime.zenMode.transitionDisposables.clear(); + this.state.runtime.zenMode.transitionDisposables.clear(); const setLineNumbers = (lineNumbers?: LineNumbersType) => { const setEditorLineNumbers = (editor: IEditor) => { @@ -1084,7 +1150,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Zen Mode Active if (this.stateModel.getRuntimeValue(LayoutStateKeys.ZEN_MODE_ACTIVE)) { - toggleFullScreen = !this.windowState.runtime.fullscreen && config.fullScreen && !isIOS; + toggleFullScreen = !this.state.runtime.fullscreen && config.fullScreen && !isIOS; if (!restoring) { zenModeExitInfo.transitionedToFullScreen = toggleFullScreen; @@ -1110,17 +1176,17 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi if (config.hideLineNumbers) { setLineNumbers('off'); - this.windowState.runtime.zenMode.transitionDisposables.add(this.editorService.onDidVisibleEditorsChange(() => setLineNumbers('off'))); + this.state.runtime.zenMode.transitionDisposables.add(this.editorService.onDidVisibleEditorsChange(() => setLineNumbers('off'))); } if (config.hideTabs && this.editorGroupService.partOptions.showTabs) { - this.windowState.runtime.zenMode.transitionDisposables.add(this.editorGroupService.enforcePartOptions({ showTabs: false })); + this.state.runtime.zenMode.transitionDisposables.add(this.editorGroupService.enforcePartOptions({ showTabs: false })); } if (config.silentNotifications && zenModeExitInfo.handleNotificationsDoNotDisturbMode) { this.notificationService.doNotDisturbMode = true; } - this.windowState.runtime.zenMode.transitionDisposables.add(this.configurationService.onDidChangeConfiguration(e => { + this.state.runtime.zenMode.transitionDisposables.add(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(WorkbenchLayoutSettings.ZEN_MODE_SILENT_NOTIFICATIONS)) { const zenModeSilentNotifications = !!this.configurationService.getValue(WorkbenchLayoutSettings.ZEN_MODE_SILENT_NOTIFICATIONS); if (zenModeExitInfo.handleNotificationsDoNotDisturbMode) { @@ -1168,7 +1234,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.focus(); - toggleFullScreen = zenModeExitInfo.transitionedToFullScreen && this.windowState.runtime.fullscreen; + toggleFullScreen = zenModeExitInfo.transitionedToFullScreen && this.state.runtime.fullscreen; } if (!skipLayout) { @@ -1188,9 +1254,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Adjust CSS if (hidden) { - this.container.classList.add(WorkbenchLayoutClasses.STATUSBAR_HIDDEN); + this.container.classList.add(LayoutClasses.STATUSBAR_HIDDEN); } else { - this.container.classList.remove(WorkbenchLayoutClasses.STATUSBAR_HIDDEN); + this.container.classList.remove(LayoutClasses.STATUSBAR_HIDDEN); } // Propagate to grid @@ -1238,7 +1304,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.container.prepend(workbenchGrid.element); this.container.setAttribute('role', 'application'); this.workbenchGrid = workbenchGrid; - this.workbenchGrid.edgeSnapping = this.windowState.runtime.fullscreen; + this.workbenchGrid.edgeSnapping = this.state.runtime.fullscreen; for (const part of [titleBar, editorPart, activityBar, panelPart, sideBar, statusBar, auxiliaryBarPart]) { this._register(part.onDidVisibilityChange((visible) => { @@ -1421,9 +1487,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Adjust CSS if (hidden) { - this.container.classList.add(WorkbenchLayoutClasses.EDITOR_HIDDEN); + this.container.classList.add(LayoutClasses.EDITOR_HIDDEN); } else { - this.container.classList.remove(WorkbenchLayoutClasses.EDITOR_HIDDEN); + this.container.classList.remove(LayoutClasses.EDITOR_HIDDEN); } // Propagate to grid @@ -1437,12 +1503,12 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi getLayoutClasses(): string[] { return coalesce([ - !this.isVisible(Parts.SIDEBAR_PART) ? WorkbenchLayoutClasses.SIDEBAR_HIDDEN : undefined, - !this.isVisible(Parts.EDITOR_PART) ? WorkbenchLayoutClasses.EDITOR_HIDDEN : undefined, - !this.isVisible(Parts.PANEL_PART) ? WorkbenchLayoutClasses.PANEL_HIDDEN : undefined, - !this.isVisible(Parts.AUXILIARYBAR_PART) ? WorkbenchLayoutClasses.AUXILIARYBAR_HIDDEN : undefined, - !this.isVisible(Parts.STATUSBAR_PART) ? WorkbenchLayoutClasses.STATUSBAR_HIDDEN : undefined, - this.windowState.runtime.fullscreen ? WorkbenchLayoutClasses.FULLSCREEN : undefined + !this.isVisible(Parts.SIDEBAR_PART) ? LayoutClasses.SIDEBAR_HIDDEN : undefined, + !this.isVisible(Parts.EDITOR_PART) ? LayoutClasses.EDITOR_HIDDEN : undefined, + !this.isVisible(Parts.PANEL_PART) ? LayoutClasses.PANEL_HIDDEN : undefined, + !this.isVisible(Parts.AUXILIARYBAR_PART) ? LayoutClasses.AUXILIARYBAR_HIDDEN : undefined, + !this.isVisible(Parts.STATUSBAR_PART) ? LayoutClasses.STATUSBAR_HIDDEN : undefined, + this.state.runtime.fullscreen ? LayoutClasses.FULLSCREEN : undefined ]); } @@ -1451,9 +1517,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Adjust CSS if (hidden) { - this.container.classList.add(WorkbenchLayoutClasses.SIDEBAR_HIDDEN); + this.container.classList.add(LayoutClasses.SIDEBAR_HIDDEN); } else { - this.container.classList.remove(WorkbenchLayoutClasses.SIDEBAR_HIDDEN); + this.container.classList.remove(LayoutClasses.SIDEBAR_HIDDEN); } // If sidebar becomes hidden, also hide the current active Viewlet if any @@ -1588,9 +1654,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Adjust CSS if (hidden) { - this.container.classList.add(WorkbenchLayoutClasses.PANEL_HIDDEN); + this.container.classList.add(LayoutClasses.PANEL_HIDDEN); } else { - this.container.classList.remove(WorkbenchLayoutClasses.PANEL_HIDDEN); + this.container.classList.remove(LayoutClasses.PANEL_HIDDEN); } // If panel part becomes hidden, also hide the current active panel if any @@ -1692,9 +1758,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Adjust CSS if (hidden) { - this.container.classList.add(WorkbenchLayoutClasses.AUXILIARYBAR_HIDDEN); + this.container.classList.add(LayoutClasses.AUXILIARYBAR_HIDDEN); } else { - this.container.classList.remove(WorkbenchLayoutClasses.AUXILIARYBAR_HIDDEN); + this.container.classList.remove(LayoutClasses.AUXILIARYBAR_HIDDEN); } // If auxiliary bar becomes hidden, also hide the current active pane composite if any @@ -1750,15 +1816,15 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } hasWindowBorder(): boolean { - return this.windowState.runtime.windowBorder; + return this.state.runtime.windowBorder; } getWindowBorderWidth(): number { - return this.windowState.runtime.windowBorder ? 2 : 0; + return this.state.runtime.windowBorder ? 2 : 0; } getWindowBorderRadius(): string | undefined { - return this.windowState.runtime.windowBorder && isMacintosh ? '5px' : undefined; + return this.state.runtime.windowBorder && isMacintosh ? '5px' : undefined; } isPanelMaximized(): boolean { @@ -1876,17 +1942,17 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } isWindowMaximized() { - return this.windowState.runtime.maximized; + return this.state.runtime.maximized; } updateWindowMaximizedState(maximized: boolean) { - this.container.classList.toggle(WorkbenchLayoutClasses.MAXIMIZED, maximized); + this.container.classList.toggle(LayoutClasses.MAXIMIZED, maximized); - if (this.windowState.runtime.maximized === maximized) { + if (this.state.runtime.maximized === maximized) { return; } - this.windowState.runtime.maximized = maximized; + this.state.runtime.maximized = maximized; this.updateWindowBorder(); this._onDidChangeWindowMaximized.fire(maximized); @@ -2172,3 +2238,292 @@ type ZenModeConfiguration = { function getZenModeConfiguration(configurationService: IConfigurationService): ZenModeConfiguration { return configurationService.getValue(WorkbenchLayoutSettings.ZEN_MODE_CONFIG); } + +//#endregion + +//#region Layout State Model + +interface IWorkbenchLayoutStateKey { + readonly name: string; + readonly runtime: boolean; + readonly defaultValue: unknown; + readonly scope: StorageScope; + readonly target: StorageTarget; + readonly zenModeIgnore?: boolean; +} + +type StorageKeyType = string | boolean | number | object; + +abstract class WorkbenchLayoutStateKey implements IWorkbenchLayoutStateKey { + + abstract readonly runtime: boolean; + + constructor(readonly name: string, readonly scope: StorageScope, readonly target: StorageTarget, public defaultValue: T) { } +} + +class RuntimeStateKey extends WorkbenchLayoutStateKey { + + readonly runtime = true; + + constructor(name: string, scope: StorageScope, target: StorageTarget, defaultValue: T, readonly zenModeIgnore?: boolean) { + super(name, scope, target, defaultValue); + } +} + +class InitializationStateKey extends WorkbenchLayoutStateKey { + readonly runtime = false; +} + +const LayoutStateKeys = { + + // Editor + EDITOR_CENTERED: new RuntimeStateKey('editor.centered', StorageScope.WORKSPACE, StorageTarget.USER, false), + + // Zen Mode + ZEN_MODE_ACTIVE: new RuntimeStateKey('zenMode.active', StorageScope.WORKSPACE, StorageTarget.USER, false), + ZEN_MODE_EXIT_INFO: new RuntimeStateKey('zenMode.exitInfo', StorageScope.WORKSPACE, StorageTarget.USER, { + transitionedToCenteredEditorLayout: false, + transitionedToFullScreen: false, + handleNotificationsDoNotDisturbMode: false, + wasVisible: { + auxiliaryBar: false, + panel: false, + sideBar: false, + }, + }), + + // Part Sizing + GRID_SIZE: new InitializationStateKey('grid.size', StorageScope.PROFILE, StorageTarget.MACHINE, { width: 800, height: 600 }), + SIDEBAR_SIZE: new InitializationStateKey('sideBar.size', StorageScope.PROFILE, StorageTarget.MACHINE, 200), + AUXILIARYBAR_SIZE: new InitializationStateKey('auxiliaryBar.size', StorageScope.PROFILE, StorageTarget.MACHINE, 200), + PANEL_SIZE: new InitializationStateKey('panel.size', StorageScope.PROFILE, StorageTarget.MACHINE, 300), + + PANEL_LAST_NON_MAXIMIZED_HEIGHT: new RuntimeStateKey('panel.lastNonMaximizedHeight', StorageScope.PROFILE, StorageTarget.MACHINE, 300), + PANEL_LAST_NON_MAXIMIZED_WIDTH: new RuntimeStateKey('panel.lastNonMaximizedWidth', StorageScope.PROFILE, StorageTarget.MACHINE, 300), + PANEL_WAS_LAST_MAXIMIZED: new RuntimeStateKey('panel.wasLastMaximized', StorageScope.WORKSPACE, StorageTarget.USER, false), + + // Part Positions + SIDEBAR_POSITON: new RuntimeStateKey('sideBar.position', StorageScope.WORKSPACE, StorageTarget.USER, Position.LEFT), + PANEL_POSITION: new RuntimeStateKey('panel.position', StorageScope.WORKSPACE, StorageTarget.USER, Position.BOTTOM), + PANEL_ALIGNMENT: new RuntimeStateKey('panel.alignment', StorageScope.PROFILE, StorageTarget.USER, 'center'), + + // Part Visibility + ACTIVITYBAR_HIDDEN: new RuntimeStateKey('activityBar.hidden', StorageScope.WORKSPACE, StorageTarget.USER, false, true), + SIDEBAR_HIDDEN: new RuntimeStateKey('sideBar.hidden', StorageScope.WORKSPACE, StorageTarget.USER, false), + EDITOR_HIDDEN: new RuntimeStateKey('editor.hidden', StorageScope.WORKSPACE, StorageTarget.USER, false), + PANEL_HIDDEN: new RuntimeStateKey('panel.hidden', StorageScope.WORKSPACE, StorageTarget.USER, true), + AUXILIARYBAR_HIDDEN: new RuntimeStateKey('auxiliaryBar.hidden', StorageScope.WORKSPACE, StorageTarget.USER, true), + STATUSBAR_HIDDEN: new RuntimeStateKey('statusBar.hidden', StorageScope.WORKSPACE, StorageTarget.USER, false, true) + +} as const; + +interface ILayoutStateChangeEvent { + readonly key: RuntimeStateKey; + readonly value: T; +} + +enum WorkbenchLayoutSettings { + PANEL_POSITION = 'workbench.panel.defaultLocation', + PANEL_OPENS_MAXIMIZED = 'workbench.panel.opensMaximized', + ZEN_MODE_CONFIG = 'zenMode', + ZEN_MODE_SILENT_NOTIFICATIONS = 'zenMode.silentNotifications', + EDITOR_CENTERED_LAYOUT_AUTO_RESIZE = 'workbench.editor.centeredLayoutAutoResize', +} + +enum LegacyWorkbenchLayoutSettings { + ACTIVITYBAR_VISIBLE = 'workbench.activityBar.visible', // Deprecated to UI State + STATUSBAR_VISIBLE = 'workbench.statusBar.visible', // Deprecated to UI State + SIDEBAR_POSITION = 'workbench.sideBar.location', // Deprecated to UI State +} + +class LayoutStateModel extends Disposable { + + static readonly STORAGE_PREFIX = 'workbench.'; + + private readonly _onDidChangeState = this._register(new Emitter>()); + readonly onDidChangeState = this._onDidChangeState.event; + + private readonly stateCache = new Map(); + + constructor( + private readonly storageService: IStorageService, + private readonly configurationService: IConfigurationService, + private readonly contextService: IWorkspaceContextService, + private readonly container: HTMLElement + ) { + super(); + + this._register(this.configurationService.onDidChangeConfiguration(configurationChange => this.updateStateFromLegacySettings(configurationChange))); + } + + private updateStateFromLegacySettings(configurationChangeEvent: IConfigurationChangeEvent): void { + const isZenMode = this.getRuntimeValue(LayoutStateKeys.ZEN_MODE_ACTIVE); + + if (configurationChangeEvent.affectsConfiguration(LegacyWorkbenchLayoutSettings.ACTIVITYBAR_VISIBLE) && !isZenMode) { + this.setRuntimeValueAndFire(LayoutStateKeys.ACTIVITYBAR_HIDDEN, !this.configurationService.getValue(LegacyWorkbenchLayoutSettings.ACTIVITYBAR_VISIBLE)); + } + + if (configurationChangeEvent.affectsConfiguration(LegacyWorkbenchLayoutSettings.STATUSBAR_VISIBLE) && !isZenMode) { + this.setRuntimeValueAndFire(LayoutStateKeys.STATUSBAR_HIDDEN, !this.configurationService.getValue(LegacyWorkbenchLayoutSettings.STATUSBAR_VISIBLE)); + } + + if (configurationChangeEvent.affectsConfiguration(LegacyWorkbenchLayoutSettings.SIDEBAR_POSITION)) { + this.setRuntimeValueAndFire(LayoutStateKeys.SIDEBAR_POSITON, positionFromString(this.configurationService.getValue(LegacyWorkbenchLayoutSettings.SIDEBAR_POSITION) ?? 'left')); + } + } + + private updateLegacySettingsFromState(key: RuntimeStateKey, value: T): void { + const isZenMode = this.getRuntimeValue(LayoutStateKeys.ZEN_MODE_ACTIVE); + if (key.zenModeIgnore && isZenMode) { + return; + } + + if (key === LayoutStateKeys.ACTIVITYBAR_HIDDEN) { + this.configurationService.updateValue(LegacyWorkbenchLayoutSettings.ACTIVITYBAR_VISIBLE, !value); + } else if (key === LayoutStateKeys.STATUSBAR_HIDDEN) { + this.configurationService.updateValue(LegacyWorkbenchLayoutSettings.STATUSBAR_VISIBLE, !value); + } else if (key === LayoutStateKeys.SIDEBAR_POSITON) { + this.configurationService.updateValue(LegacyWorkbenchLayoutSettings.SIDEBAR_POSITION, positionToString(value as Position)); + } + } + + load(): void { + let key: keyof typeof LayoutStateKeys; + + // Load stored values for all keys + for (key in LayoutStateKeys) { + const stateKey = LayoutStateKeys[key] as WorkbenchLayoutStateKey; + const value = this.loadKeyFromStorage(stateKey); + + if (value !== undefined) { + this.stateCache.set(stateKey.name, value); + } + } + + // Apply legacy settings + this.stateCache.set(LayoutStateKeys.ACTIVITYBAR_HIDDEN.name, !this.configurationService.getValue(LegacyWorkbenchLayoutSettings.ACTIVITYBAR_VISIBLE)); + this.stateCache.set(LayoutStateKeys.STATUSBAR_HIDDEN.name, !this.configurationService.getValue(LegacyWorkbenchLayoutSettings.STATUSBAR_VISIBLE)); + this.stateCache.set(LayoutStateKeys.SIDEBAR_POSITON.name, positionFromString(this.configurationService.getValue(LegacyWorkbenchLayoutSettings.SIDEBAR_POSITION) ?? 'left')); + + // Set dynamic defaults: part sizing and side bar visibility + const workbenchDimensions = getClientArea(this.container); + LayoutStateKeys.PANEL_POSITION.defaultValue = positionFromString(this.configurationService.getValue(WorkbenchLayoutSettings.PANEL_POSITION) ?? 'bottom'); + LayoutStateKeys.GRID_SIZE.defaultValue = { height: workbenchDimensions.height, width: workbenchDimensions.width }; + LayoutStateKeys.SIDEBAR_SIZE.defaultValue = Math.min(300, workbenchDimensions.width / 4); + LayoutStateKeys.AUXILIARYBAR_SIZE.defaultValue = Math.min(300, workbenchDimensions.width / 4); + LayoutStateKeys.PANEL_SIZE.defaultValue = (this.stateCache.get(LayoutStateKeys.PANEL_POSITION.name) ?? LayoutStateKeys.PANEL_POSITION.defaultValue) === 'bottom' ? workbenchDimensions.height / 3 : workbenchDimensions.width / 4; + LayoutStateKeys.SIDEBAR_HIDDEN.defaultValue = this.contextService.getWorkbenchState() === WorkbenchState.EMPTY; + + // Apply all defaults + for (key in LayoutStateKeys) { + const stateKey = LayoutStateKeys[key]; + if (this.stateCache.get(stateKey.name) === undefined) { + this.stateCache.set(stateKey.name, stateKey.defaultValue); + } + } + + // Register for runtime key changes + this._register(this.storageService.onDidChangeValue(storageChangeEvent => { + let key: keyof typeof LayoutStateKeys; + for (key in LayoutStateKeys) { + const stateKey = LayoutStateKeys[key] as WorkbenchLayoutStateKey; + if (stateKey instanceof RuntimeStateKey && stateKey.scope === StorageScope.PROFILE && stateKey.target === StorageTarget.USER) { + if (`${LayoutStateModel.STORAGE_PREFIX}${stateKey.name}` === storageChangeEvent.key) { + const value = this.loadKeyFromStorage(stateKey) ?? stateKey.defaultValue; + if (this.stateCache.get(stateKey.name) !== value) { + this.stateCache.set(stateKey.name, value); + this._onDidChangeState.fire({ key: stateKey, value }); + } + } + } + } + })); + } + + save(workspace: boolean, global: boolean): void { + let key: keyof typeof LayoutStateKeys; + + const isZenMode = this.getRuntimeValue(LayoutStateKeys.ZEN_MODE_ACTIVE); + + for (key in LayoutStateKeys) { + const stateKey = LayoutStateKeys[key] as WorkbenchLayoutStateKey; + if ((workspace && stateKey.scope === StorageScope.WORKSPACE) || + (global && stateKey.scope === StorageScope.PROFILE)) { + if (isZenMode && stateKey instanceof RuntimeStateKey && stateKey.zenModeIgnore) { + continue; // Don't write out specific keys while in zen mode + } + + this.saveKeyToStorage(stateKey); + } + } + } + + getInitializationValue(key: InitializationStateKey): T { + return this.stateCache.get(key.name) as T; + } + + setInitializationValue(key: InitializationStateKey, value: T): void { + this.stateCache.set(key.name, value); + } + + getRuntimeValue(key: RuntimeStateKey, fallbackToSetting?: boolean): T { + if (fallbackToSetting) { + switch (key) { + case LayoutStateKeys.ACTIVITYBAR_HIDDEN: + this.stateCache.set(key.name, !this.configurationService.getValue(LegacyWorkbenchLayoutSettings.ACTIVITYBAR_VISIBLE)); + break; + case LayoutStateKeys.STATUSBAR_HIDDEN: + this.stateCache.set(key.name, !this.configurationService.getValue(LegacyWorkbenchLayoutSettings.STATUSBAR_VISIBLE)); + break; + case LayoutStateKeys.SIDEBAR_POSITON: + this.stateCache.set(key.name, this.configurationService.getValue(LegacyWorkbenchLayoutSettings.SIDEBAR_POSITION) ?? 'left'); + break; + } + } + + return this.stateCache.get(key.name) as T; + } + + setRuntimeValue(key: RuntimeStateKey, value: T): void { + this.stateCache.set(key.name, value); + const isZenMode = this.getRuntimeValue(LayoutStateKeys.ZEN_MODE_ACTIVE); + + if (key.scope === StorageScope.PROFILE) { + if (!isZenMode || !key.zenModeIgnore) { + this.saveKeyToStorage(key); + this.updateLegacySettingsFromState(key, value); + } + } + } + + private setRuntimeValueAndFire(key: RuntimeStateKey, value: T): void { + const previousValue = this.stateCache.get(key.name); + if (previousValue === value) { + return; + } + + this.setRuntimeValue(key, value); + this._onDidChangeState.fire({ key, value }); + } + + private saveKeyToStorage(key: WorkbenchLayoutStateKey): void { + const value = this.stateCache.get(key.name) as T; + this.storageService.store(`${LayoutStateModel.STORAGE_PREFIX}${key.name}`, typeof value === 'object' ? JSON.stringify(value) : value, key.scope, key.target); + } + + private loadKeyFromStorage(key: WorkbenchLayoutStateKey): T | undefined { + let value: any = this.storageService.get(`${LayoutStateModel.STORAGE_PREFIX}${key.name}`, key.scope); + + if (value !== undefined) { + switch (typeof key.defaultValue) { + case 'boolean': value = value === 'true'; break; + case 'number': value = parseInt(value); break; + case 'object': value = JSON.parse(value); break; + } + } + + return value as T | undefined; + } +} + +//#endregion diff --git a/src/vs/workbench/browser/layoutState.ts b/src/vs/workbench/browser/layoutState.ts deleted file mode 100644 index 4df700b69d7..00000000000 --- a/src/vs/workbench/browser/layoutState.ts +++ /dev/null @@ -1,295 +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 { getClientArea } from 'vs/base/browser/dom'; -import { Emitter } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { IConfigurationChangeEvent, IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { PanelAlignment, Position, positionFromString, positionToString } from 'vs/workbench/services/layout/browser/layoutService'; - -interface IWorkbenchLayoutStateKey { - readonly name: string; - readonly runtime: boolean; - readonly defaultValue: unknown; - readonly scope: StorageScope; - readonly target: StorageTarget; - readonly zenModeIgnore?: boolean; -} - -type StorageKeyType = string | boolean | number | object; - -abstract class WorkbenchLayoutStateKey implements IWorkbenchLayoutStateKey { - - abstract readonly runtime: boolean; - - constructor(readonly name: string, readonly scope: StorageScope, readonly target: StorageTarget, public defaultValue: T) { } -} - -class RuntimeStateKey extends WorkbenchLayoutStateKey { - - readonly runtime = true; - - constructor(name: string, scope: StorageScope, target: StorageTarget, defaultValue: T, readonly zenModeIgnore?: boolean) { - super(name, scope, target, defaultValue); - } -} - -class InitializationStateKey extends WorkbenchLayoutStateKey { - readonly runtime = false; -} - -export const LayoutStateKeys = { - - // Editor - EDITOR_CENTERED: new RuntimeStateKey('editor.centered', StorageScope.WORKSPACE, StorageTarget.USER, false), - - // Zen Mode - ZEN_MODE_ACTIVE: new RuntimeStateKey('zenMode.active', StorageScope.WORKSPACE, StorageTarget.USER, false), - ZEN_MODE_EXIT_INFO: new RuntimeStateKey('zenMode.exitInfo', StorageScope.WORKSPACE, StorageTarget.USER, { - transitionedToCenteredEditorLayout: false, - transitionedToFullScreen: false, - handleNotificationsDoNotDisturbMode: false, - wasVisible: { - auxiliaryBar: false, - panel: false, - sideBar: false, - }, - }), - - // Part Sizing - GRID_SIZE: new InitializationStateKey('grid.size', StorageScope.PROFILE, StorageTarget.MACHINE, { width: 800, height: 600 }), - SIDEBAR_SIZE: new InitializationStateKey('sideBar.size', StorageScope.PROFILE, StorageTarget.MACHINE, 200), - AUXILIARYBAR_SIZE: new InitializationStateKey('auxiliaryBar.size', StorageScope.PROFILE, StorageTarget.MACHINE, 200), - PANEL_SIZE: new InitializationStateKey('panel.size', StorageScope.PROFILE, StorageTarget.MACHINE, 300), - - PANEL_LAST_NON_MAXIMIZED_HEIGHT: new RuntimeStateKey('panel.lastNonMaximizedHeight', StorageScope.PROFILE, StorageTarget.MACHINE, 300), - PANEL_LAST_NON_MAXIMIZED_WIDTH: new RuntimeStateKey('panel.lastNonMaximizedWidth', StorageScope.PROFILE, StorageTarget.MACHINE, 300), - PANEL_WAS_LAST_MAXIMIZED: new RuntimeStateKey('panel.wasLastMaximized', StorageScope.WORKSPACE, StorageTarget.USER, false), - - // Part Positions - SIDEBAR_POSITON: new RuntimeStateKey('sideBar.position', StorageScope.WORKSPACE, StorageTarget.USER, Position.LEFT), - PANEL_POSITION: new RuntimeStateKey('panel.position', StorageScope.WORKSPACE, StorageTarget.USER, Position.BOTTOM), - PANEL_ALIGNMENT: new RuntimeStateKey('panel.alignment', StorageScope.PROFILE, StorageTarget.USER, 'center'), - - // Part Visibility - ACTIVITYBAR_HIDDEN: new RuntimeStateKey('activityBar.hidden', StorageScope.WORKSPACE, StorageTarget.USER, false, true), - SIDEBAR_HIDDEN: new RuntimeStateKey('sideBar.hidden', StorageScope.WORKSPACE, StorageTarget.USER, false), - EDITOR_HIDDEN: new RuntimeStateKey('editor.hidden', StorageScope.WORKSPACE, StorageTarget.USER, false), - PANEL_HIDDEN: new RuntimeStateKey('panel.hidden', StorageScope.WORKSPACE, StorageTarget.USER, true), - AUXILIARYBAR_HIDDEN: new RuntimeStateKey('auxiliaryBar.hidden', StorageScope.WORKSPACE, StorageTarget.USER, true), - STATUSBAR_HIDDEN: new RuntimeStateKey('statusBar.hidden', StorageScope.WORKSPACE, StorageTarget.USER, false, true) - -} as const; - -interface ILayoutStateChangeEvent { - readonly key: RuntimeStateKey; - readonly value: T; -} - -export enum WorkbenchLayoutSettings { - PANEL_POSITION = 'workbench.panel.defaultLocation', - PANEL_OPENS_MAXIMIZED = 'workbench.panel.opensMaximized', - ZEN_MODE_CONFIG = 'zenMode', - ZEN_MODE_SILENT_NOTIFICATIONS = 'zenMode.silentNotifications', - EDITOR_CENTERED_LAYOUT_AUTO_RESIZE = 'workbench.editor.centeredLayoutAutoResize', -} - -enum LegacyWorkbenchLayoutSettings { - ACTIVITYBAR_VISIBLE = 'workbench.activityBar.visible', // Deprecated to UI State - STATUSBAR_VISIBLE = 'workbench.statusBar.visible', // Deprecated to UI State - SIDEBAR_POSITION = 'workbench.sideBar.location', // Deprecated to UI State -} - -export class LayoutStateModel extends Disposable { - - static readonly STORAGE_PREFIX = 'workbench.'; - - private readonly _onDidChangeState = this._register(new Emitter>()); - readonly onDidChangeState = this._onDidChangeState.event; - - private readonly stateCache = new Map(); - - constructor( - private readonly storageService: IStorageService, - private readonly configurationService: IConfigurationService, - private readonly contextService: IWorkspaceContextService, - private readonly container: HTMLElement - ) { - super(); - - this._register(this.configurationService.onDidChangeConfiguration(configurationChange => this.updateStateFromLegacySettings(configurationChange))); - } - - private updateStateFromLegacySettings(configurationChangeEvent: IConfigurationChangeEvent): void { - const isZenMode = this.getRuntimeValue(LayoutStateKeys.ZEN_MODE_ACTIVE); - - if (configurationChangeEvent.affectsConfiguration(LegacyWorkbenchLayoutSettings.ACTIVITYBAR_VISIBLE) && !isZenMode) { - this.setRuntimeValueAndFire(LayoutStateKeys.ACTIVITYBAR_HIDDEN, !this.configurationService.getValue(LegacyWorkbenchLayoutSettings.ACTIVITYBAR_VISIBLE)); - } - - if (configurationChangeEvent.affectsConfiguration(LegacyWorkbenchLayoutSettings.STATUSBAR_VISIBLE) && !isZenMode) { - this.setRuntimeValueAndFire(LayoutStateKeys.STATUSBAR_HIDDEN, !this.configurationService.getValue(LegacyWorkbenchLayoutSettings.STATUSBAR_VISIBLE)); - } - - if (configurationChangeEvent.affectsConfiguration(LegacyWorkbenchLayoutSettings.SIDEBAR_POSITION)) { - this.setRuntimeValueAndFire(LayoutStateKeys.SIDEBAR_POSITON, positionFromString(this.configurationService.getValue(LegacyWorkbenchLayoutSettings.SIDEBAR_POSITION) ?? 'left')); - } - } - - private updateLegacySettingsFromState(key: RuntimeStateKey, value: T): void { - const isZenMode = this.getRuntimeValue(LayoutStateKeys.ZEN_MODE_ACTIVE); - if (key.zenModeIgnore && isZenMode) { - return; - } - - if (key === LayoutStateKeys.ACTIVITYBAR_HIDDEN) { - this.configurationService.updateValue(LegacyWorkbenchLayoutSettings.ACTIVITYBAR_VISIBLE, !value); - } else if (key === LayoutStateKeys.STATUSBAR_HIDDEN) { - this.configurationService.updateValue(LegacyWorkbenchLayoutSettings.STATUSBAR_VISIBLE, !value); - } else if (key === LayoutStateKeys.SIDEBAR_POSITON) { - this.configurationService.updateValue(LegacyWorkbenchLayoutSettings.SIDEBAR_POSITION, positionToString(value as Position)); - } - } - - load(): void { - let key: keyof typeof LayoutStateKeys; - - // Load stored values for all keys - for (key in LayoutStateKeys) { - const stateKey = LayoutStateKeys[key] as WorkbenchLayoutStateKey; - const value = this.loadKeyFromStorage(stateKey); - - if (value !== undefined) { - this.stateCache.set(stateKey.name, value); - } - } - - // Apply legacy settings - this.stateCache.set(LayoutStateKeys.ACTIVITYBAR_HIDDEN.name, !this.configurationService.getValue(LegacyWorkbenchLayoutSettings.ACTIVITYBAR_VISIBLE)); - this.stateCache.set(LayoutStateKeys.STATUSBAR_HIDDEN.name, !this.configurationService.getValue(LegacyWorkbenchLayoutSettings.STATUSBAR_VISIBLE)); - this.stateCache.set(LayoutStateKeys.SIDEBAR_POSITON.name, positionFromString(this.configurationService.getValue(LegacyWorkbenchLayoutSettings.SIDEBAR_POSITION) ?? 'left')); - - // Set dynamic defaults: part sizing and side bar visibility - const workbenchDimensions = getClientArea(this.container); - LayoutStateKeys.PANEL_POSITION.defaultValue = positionFromString(this.configurationService.getValue(WorkbenchLayoutSettings.PANEL_POSITION) ?? 'bottom'); - LayoutStateKeys.GRID_SIZE.defaultValue = { height: workbenchDimensions.height, width: workbenchDimensions.width }; - LayoutStateKeys.SIDEBAR_SIZE.defaultValue = Math.min(300, workbenchDimensions.width / 4); - LayoutStateKeys.AUXILIARYBAR_SIZE.defaultValue = Math.min(300, workbenchDimensions.width / 4); - LayoutStateKeys.PANEL_SIZE.defaultValue = (this.stateCache.get(LayoutStateKeys.PANEL_POSITION.name) ?? LayoutStateKeys.PANEL_POSITION.defaultValue) === 'bottom' ? workbenchDimensions.height / 3 : workbenchDimensions.width / 4; - LayoutStateKeys.SIDEBAR_HIDDEN.defaultValue = this.contextService.getWorkbenchState() === WorkbenchState.EMPTY; - - // Apply all defaults - for (key in LayoutStateKeys) { - const stateKey = LayoutStateKeys[key]; - if (this.stateCache.get(stateKey.name) === undefined) { - this.stateCache.set(stateKey.name, stateKey.defaultValue); - } - } - - // Register for runtime key changes - this._register(this.storageService.onDidChangeValue(storageChangeEvent => { - let key: keyof typeof LayoutStateKeys; - for (key in LayoutStateKeys) { - const stateKey = LayoutStateKeys[key] as WorkbenchLayoutStateKey; - if (stateKey instanceof RuntimeStateKey && stateKey.scope === StorageScope.PROFILE && stateKey.target === StorageTarget.USER) { - if (`${LayoutStateModel.STORAGE_PREFIX}${stateKey.name}` === storageChangeEvent.key) { - const value = this.loadKeyFromStorage(stateKey) ?? stateKey.defaultValue; - if (this.stateCache.get(stateKey.name) !== value) { - this.stateCache.set(stateKey.name, value); - this._onDidChangeState.fire({ key: stateKey, value }); - } - } - } - } - })); - } - - save(workspace: boolean, global: boolean): void { - let key: keyof typeof LayoutStateKeys; - - const isZenMode = this.getRuntimeValue(LayoutStateKeys.ZEN_MODE_ACTIVE); - - for (key in LayoutStateKeys) { - const stateKey = LayoutStateKeys[key] as WorkbenchLayoutStateKey; - if ((workspace && stateKey.scope === StorageScope.WORKSPACE) || - (global && stateKey.scope === StorageScope.PROFILE)) { - if (isZenMode && stateKey instanceof RuntimeStateKey && stateKey.zenModeIgnore) { - continue; // Don't write out specific keys while in zen mode - } - - this.saveKeyToStorage(stateKey); - } - } - } - - getInitializationValue(key: InitializationStateKey): T { - return this.stateCache.get(key.name) as T; - } - - setInitializationValue(key: InitializationStateKey, value: T): void { - this.stateCache.set(key.name, value); - } - - getRuntimeValue(key: RuntimeStateKey, fallbackToSetting?: boolean): T { - if (fallbackToSetting) { - switch (key) { - case LayoutStateKeys.ACTIVITYBAR_HIDDEN: - this.stateCache.set(key.name, !this.configurationService.getValue(LegacyWorkbenchLayoutSettings.ACTIVITYBAR_VISIBLE)); - break; - case LayoutStateKeys.STATUSBAR_HIDDEN: - this.stateCache.set(key.name, !this.configurationService.getValue(LegacyWorkbenchLayoutSettings.STATUSBAR_VISIBLE)); - break; - case LayoutStateKeys.SIDEBAR_POSITON: - this.stateCache.set(key.name, this.configurationService.getValue(LegacyWorkbenchLayoutSettings.SIDEBAR_POSITION) ?? 'left'); - break; - } - } - - return this.stateCache.get(key.name) as T; - } - - setRuntimeValue(key: RuntimeStateKey, value: T): void { - this.stateCache.set(key.name, value); - const isZenMode = this.getRuntimeValue(LayoutStateKeys.ZEN_MODE_ACTIVE); - - if (key.scope === StorageScope.PROFILE) { - if (!isZenMode || !key.zenModeIgnore) { - this.saveKeyToStorage(key); - this.updateLegacySettingsFromState(key, value); - } - } - } - - private setRuntimeValueAndFire(key: RuntimeStateKey, value: T): void { - const previousValue = this.stateCache.get(key.name); - if (previousValue === value) { - return; - } - - this.setRuntimeValue(key, value); - this._onDidChangeState.fire({ key, value }); - } - - private saveKeyToStorage(key: WorkbenchLayoutStateKey): void { - const value = this.stateCache.get(key.name) as T; - this.storageService.store(`${LayoutStateModel.STORAGE_PREFIX}${key.name}`, typeof value === 'object' ? JSON.stringify(value) : value, key.scope, key.target); - } - - private loadKeyFromStorage(key: WorkbenchLayoutStateKey): T | undefined { - let value: any = this.storageService.get(`${LayoutStateModel.STORAGE_PREFIX}${key.name}`, key.scope); - - if (value !== undefined) { - switch (typeof key.defaultValue) { - case 'boolean': value = value === 'true'; break; - case 'number': value = parseInt(value); break; - case 'object': value = JSON.parse(value); break; - } - } - - return value as T | undefined; - } -} diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index fd4c88dd6e0..bee1974abd0 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -16,10 +16,10 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { activeContrastBorder, focusBorder } from 'vs/platform/theme/common/colorRegistry'; import { IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { ActivityAction, ActivityActionViewItem, IActivityHoverOptions, ICompositeBar, ICompositeBarColors, ToggleCompositePinnedAction } from 'vs/workbench/browser/parts/compositeBarActions'; +import { ActivityAction, ActivityActionViewItem, IActivityActionViewItemOptions, IActivityHoverOptions, ICompositeBar, ICompositeBarColors, ToggleCompositePinnedAction } from 'vs/workbench/browser/parts/compositeBarActions'; import { CATEGORIES } from 'vs/workbench/common/actions'; import { IActivity } from 'vs/workbench/common/activity'; -import { ACTIVITY_BAR_FOREGROUND, ACTIVITY_BAR_ACTIVE_BORDER, ACTIVITY_BAR_ACTIVE_FOCUS_BORDER, ACTIVITY_BAR_ACTIVE_BACKGROUND } from 'vs/workbench/common/theme'; +import { ACTIVITY_BAR_FOREGROUND, ACTIVITY_BAR_ACTIVE_BORDER, ACTIVITY_BAR_ACTIVE_FOCUS_BORDER, ACTIVITY_BAR_ACTIVE_BACKGROUND, ACTIVITY_BAR_SETTINGS_PROFILE_HOVER_FOREGROUND } from 'vs/workbench/common/theme'; import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; @@ -37,6 +37,8 @@ import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/b import { ViewContainerLocation } from 'vs/workbench/common/views'; import { IPaneCompositePart } from 'vs/workbench/browser/parts/paneCompositePart'; import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IUserDataProfileService, MANAGE_PROFILES_ACTION_ID, PROFILES_CATEGORY } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; export class ViewContainerActivityAction extends ActivityAction { @@ -109,14 +111,12 @@ export class ViewContainerActivityAction extends ActivityAction { } } -class MenuActivityActionViewItem extends ActivityActionViewItem { +abstract class AbstractGlobalActivityActionViewItem extends ActivityActionViewItem { constructor( - private readonly menuId: MenuId, action: ActivityAction, private contextMenuActionsProvider: () => IAction[], - colors: (theme: IColorTheme) => ICompositeBarColors, - hoverOptions: IActivityHoverOptions, + options: IActivityActionViewItemOptions, @IThemeService themeService: IThemeService, @IHoverService hoverService: IHoverService, @IMenuService protected readonly menuService: IMenuService, @@ -126,7 +126,7 @@ class MenuActivityActionViewItem extends ActivityActionViewItem { @IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService, @IKeybindingService keybindingService: IKeybindingService, ) { - super(action, { draggable: false, colors, icon: true, hasPopup: true, hoverOptions }, themeService, hoverService, configurationService, keybindingService); + super(action, options, themeService, hoverService, configurationService, keybindingService); } override render(container: HTMLElement): void { @@ -135,60 +135,84 @@ class MenuActivityActionViewItem extends ActivityActionViewItem { // Context menus are triggered on mouse down so that an item can be picked // and executed with releasing the mouse over it - this._register(addDisposableListener(this.container, EventType.MOUSE_DOWN, (e: MouseEvent) => { + this._register(addDisposableListener(this.container, EventType.MOUSE_DOWN, async (e: MouseEvent) => { EventHelper.stop(e, true); - this.showContextMenu(e); + const isLeftClick = e?.button !== 2; + // Left-click run + if (isLeftClick) { + this.run(); + } else { + const disposables = new DisposableStore(); + const actions = await this.resolveContextMenuActions(disposables); + + const elementPosition = getDomNodePagePosition(this.container); + const anchor = { + x: Math.floor(elementPosition.left + (elementPosition.width / 2)), + y: elementPosition.top + elementPosition.height + }; + + this.contextMenuService.showContextMenu({ + getAnchor: () => anchor, + getActions: () => actions, + onHide: () => disposables.dispose() + }); + } })); this._register(addDisposableListener(this.container, EventType.KEY_UP, (e: KeyboardEvent) => { const event = new StandardKeyboardEvent(e); if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { EventHelper.stop(e, true); - this.showContextMenu(); + this.run(); } })); this._register(addDisposableListener(this.container, TouchEventType.Tap, (e: GestureEvent) => { EventHelper.stop(e, true); - this.showContextMenu(); + this.run(); })); } - private async showContextMenu(e?: MouseEvent): Promise { + protected async resolveContextMenuActions(disposables: DisposableStore): Promise { + return this.contextMenuActionsProvider(); + } + + protected abstract run(): Promise; +} + +class MenuActivityActionViewItem extends AbstractGlobalActivityActionViewItem { + + constructor( + private readonly menuId: MenuId, + action: ActivityAction, + contextMenuActionsProvider: () => IAction[], + colors: (theme: IColorTheme) => ICompositeBarColors, + hoverOptions: IActivityHoverOptions, + @IThemeService themeService: IThemeService, + @IHoverService hoverService: IHoverService, + @IMenuService menuService: IMenuService, + @IContextMenuService contextMenuService: IContextMenuService, + @IContextKeyService contextKeyService: IContextKeyService, + @IConfigurationService configurationService: IConfigurationService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IKeybindingService keybindingService: IKeybindingService, + ) { + super(action, contextMenuActionsProvider, { draggable: false, colors, icon: true, hasPopup: true, hoverOptions }, themeService, hoverService, menuService, contextMenuService, contextKeyService, configurationService, environmentService, keybindingService); + } + + protected async run(): Promise { const disposables = new DisposableStore(); + const menu = disposables.add(this.menuService.createMenu(this.menuId, this.contextKeyService)); + const actions = await this.resolveMainMenuActions(menu, disposables); - const isLeftClick = e?.button !== 2; + this.contextMenuService.showContextMenu({ + getAnchor: () => this.container, + anchorAlignment: this.configurationService.getValue('workbench.sideBar.location') === 'left' ? AnchorAlignment.RIGHT : AnchorAlignment.LEFT, + anchorAxisAlignment: AnchorAxisAlignment.HORIZONTAL, + getActions: () => actions, + onHide: () => disposables.dispose() + }); - // Left-click main menu - if (isLeftClick) { - const menu = disposables.add(this.menuService.createMenu(this.menuId, this.contextKeyService)); - const actions = await this.resolveMainMenuActions(menu, disposables); - - this.contextMenuService.showContextMenu({ - getAnchor: () => this.container, - anchorAlignment: this.configurationService.getValue('workbench.sideBar.location') === 'left' ? AnchorAlignment.RIGHT : AnchorAlignment.LEFT, - anchorAxisAlignment: AnchorAxisAlignment.HORIZONTAL, - getActions: () => actions, - onHide: () => disposables.dispose() - }); - } - - // Right-click context menu - else { - const actions = await this.resolveContextMenuActions(disposables); - - const elementPosition = getDomNodePagePosition(this.container); - const anchor = { - x: Math.floor(elementPosition.left + (elementPosition.width / 2)), - y: elementPosition.top + elementPosition.height - }; - - this.contextMenuService.showContextMenu({ - getAnchor: () => anchor, - getActions: () => actions, - onHide: () => disposables.dispose() - }); - } } protected async resolveMainMenuActions(menu: IMenu, _disposable: DisposableStore): Promise { @@ -197,9 +221,6 @@ class MenuActivityActionViewItem extends ActivityActionViewItem { return actions; } - protected async resolveContextMenuActions(disposables: DisposableStore): Promise { - return this.contextMenuActionsProvider(); - } } export class AccountsActivityActionViewItem extends MenuActivityActionViewItem { @@ -289,14 +310,14 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem { } if (menus.length && otherCommands.length) { - menus.push(disposables.add(new Separator())); + menus.push(new Separator()); } otherCommands.forEach((group, i) => { const actions = group[1]; menus = menus.concat(actions); if (i !== otherCommands.length - 1) { - menus.push(disposables.add(new Separator())); + menus.push(new Separator()); } }); @@ -315,6 +336,55 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem { } } +export interface IProfileActivity extends IActivity { + readonly icon: boolean; +} + +export class ProfilesActivityActionViewItem extends AbstractGlobalActivityActionViewItem { + + static readonly PROFILES_VISIBILITY_PREFERENCE_KEY = 'workbench.activity.showProfiles'; + + constructor( + action: ActivityAction, + contextMenuActionsProvider: () => IAction[], + colors: (theme: IColorTheme) => ICompositeBarColors, + hoverOptions: IActivityHoverOptions, + @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, + @ICommandService private readonly commandService: ICommandService, + @IStorageService private readonly storageService: IStorageService, + @IThemeService themeService: IThemeService, + @IHoverService hoverService: IHoverService, + @IMenuService menuService: IMenuService, + @IContextMenuService contextMenuService: IContextMenuService, + @IContextKeyService contextKeyService: IContextKeyService, + @IConfigurationService configurationService: IConfigurationService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IKeybindingService keybindingService: IKeybindingService, + ) { + super(action, contextMenuActionsProvider, { draggable: false, colors, icon: (action.activity).icon, hasPopup: true, hoverOptions }, themeService, hoverService, menuService, contextMenuService, contextKeyService, configurationService, environmentService, keybindingService); + } + + protected run(): Promise { + return this.commandService.executeCommand(MANAGE_PROFILES_ACTION_ID); + } + + protected override async resolveContextMenuActions(disposables: DisposableStore): Promise { + const actions = await super.resolveContextMenuActions(disposables); + + actions.unshift(...[ + toAction({ id: 'hideprofiles', label: localize('hideprofiles', "Hide {0}", PROFILES_CATEGORY), run: () => this.storageService.store(ProfilesActivityActionViewItem.PROFILES_VISIBILITY_PREFERENCE_KEY, false, StorageScope.PROFILE, StorageTarget.USER) }), + new Separator() + ]); + + return actions; + } + + protected override computeTitle(): string { + return localize('profiles', "{0} (Settings Profile)", this.userDataProfileService.currentProfile.name); + } + +} + export class GlobalActivityActionViewItem extends MenuActivityActionViewItem { constructor( @@ -426,9 +496,9 @@ registerThemingParticipant((theme, collector) => { const activityBarForegroundColor = theme.getColor(ACTIVITY_BAR_FOREGROUND); if (activityBarForegroundColor) { collector.addRule(` - .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.active .action-label:not(.codicon), - .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item:focus .action-label:not(.codicon), - .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item:hover .action-label:not(.codicon) { + .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.active .action-label:not(.codicon):not(.profile-activity-item), + .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item:focus .action-label:not(.codicon):not(.profile-activity-item), + .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item:hover .action-label:not(.codicon):not(.profile-activity-item) { background-color: ${activityBarForegroundColor} !important; } .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.active .action-label.codicon, @@ -439,6 +509,17 @@ registerThemingParticipant((theme, collector) => { `); } + const activityBarSettingsProfileHoveFgColor = theme.getColor(ACTIVITY_BAR_SETTINGS_PROFILE_HOVER_FOREGROUND); + if (activityBarSettingsProfileHoveFgColor) { + collector.addRule(` + .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.active .action-label.profile-activity-item, + .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item:focus .action-label.profile-activity-item, + .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item:hover .action-label.profile-activity-item { + color: ${activityBarSettingsProfileHoveFgColor} !important; + } + `); + } + const activityBarActiveBorderColor = theme.getColor(ACTIVITY_BAR_ACTIVE_BORDER); if (activityBarActiveBorderColor) { collector.addRule(` diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 6d850ae03eb..2c6a94b9877 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -8,14 +8,15 @@ import { localize } from 'vs/nls'; import { ActionsOrientation, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { GLOBAL_ACTIVITY_ID, IActivity, ACCOUNTS_ACTIVITY_ID } from 'vs/workbench/common/activity'; import { Part } from 'vs/workbench/browser/part'; -import { GlobalActivityActionViewItem, ViewContainerActivityAction, PlaceHolderToggleCompositePinnedAction, PlaceHolderViewContainerActivityAction, AccountsActivityActionViewItem } from 'vs/workbench/browser/parts/activitybar/activitybarActions'; +import { GlobalActivityActionViewItem, ViewContainerActivityAction, PlaceHolderToggleCompositePinnedAction, PlaceHolderViewContainerActivityAction, AccountsActivityActionViewItem, ProfilesActivityActionViewItem, IProfileActivity } from 'vs/workbench/browser/parts/activitybar/activitybarActions'; import { IBadge, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IWorkbenchLayoutService, Parts, Position } from 'vs/workbench/services/layout/browser/layoutService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IDisposable, toDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; +import { Event } from 'vs/base/common/event'; import { ToggleActivityBarVisibilityAction, ToggleSidebarPositionAction } from 'vs/workbench/browser/actions/layoutActions'; import { IThemeService, IColorTheme, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { ACTIVITY_BAR_BACKGROUND, ACTIVITY_BAR_BORDER, ACTIVITY_BAR_FOREGROUND, ACTIVITY_BAR_ACTIVE_BORDER, ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND, ACTIVITY_BAR_INACTIVE_FOREGROUND, ACTIVITY_BAR_ACTIVE_BACKGROUND, ACTIVITY_BAR_DRAG_AND_DROP_BORDER } from 'vs/workbench/common/theme'; +import { ACTIVITY_BAR_BACKGROUND, ACTIVITY_BAR_BORDER, ACTIVITY_BAR_FOREGROUND, ACTIVITY_BAR_ACTIVE_BORDER, ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND, ACTIVITY_BAR_INACTIVE_FOREGROUND, ACTIVITY_BAR_ACTIVE_BACKGROUND, ACTIVITY_BAR_DRAG_AND_DROP_BORDER, ACTIVITY_BAR_SETTINGS_PROFILE_FOREGROUND } from 'vs/workbench/common/theme'; import { contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { CompositeBar, ICompositeBarItem, CompositeDragAndDrop } from 'vs/workbench/browser/parts/compositeBar'; import { Dimension, createCSSRule, asCSSUrl, addDisposableListener, EventType, isAncestor } from 'vs/base/browser/dom'; @@ -44,6 +45,8 @@ import { GestureEvent } from 'vs/base/browser/touch'; import { IPaneCompositePart, IPaneCompositeSelectorPart } from 'vs/workbench/browser/parts/paneCompositePart'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions, IProfileStorageRegistry } from 'vs/workbench/services/userDataProfile/common/userDataProfileStorageRegistry'; +import { IUserDataProfileService, PROFILES_TTILE } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; interface IPlaceholderViewContainer { readonly id: string; @@ -107,6 +110,7 @@ export class ActivitybarPart extends Part implements IPaneCompositeSelectorPart private readonly globalActivity: ICompositeActivity[] = []; private accountsActivityAction: ActivityAction | undefined; + private profilesActivityAction: ActivityAction | undefined; private readonly accountsActivity: ICompositeActivity[] = []; @@ -131,6 +135,8 @@ export class ActivitybarPart extends Part implements IPaneCompositeSelectorPart @IContextKeyService private readonly contextKeyService: IContextKeyService, @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, ) { super(Parts.ACTIVITYBAR_PART, { hasTitle: false }, themeService, storageService, layoutService); @@ -190,6 +196,7 @@ export class ActivitybarPart extends Part implements IPaneCompositeSelectorPart // Accounts actions.push(new Separator()); actions.push(toAction({ id: 'toggleAccountsVisibility', label: localize('accounts', "Accounts"), checked: this.accountsVisibilityPreference, run: () => this.accountsVisibilityPreference = !this.accountsVisibilityPreference })); + actions.push(toAction({ id: 'toggleProfilesVisibility', label: PROFILES_TTILE.value, checked: this.profilesVisibilityPreference, run: () => this.profilesVisibilityPreference = !this.profilesVisibilityPreference })); actions.push(new Separator()); // Toggle Sidebar @@ -268,6 +275,9 @@ export class ActivitybarPart extends Part implements IPaneCompositeSelectorPart } } })); + + this._register(this.userDataProfilesService.onDidChangeProfiles(() => this.toggleProfilesActivityAction())); + this._register(Event.any(this.userDataProfileService.onDidChangeCurrentProfile, this.userDataProfileService.onDidUpdateCurrentProfile)(() => this.updateProfilesActivityAction())); } private onDidChangeViewContainers(added: readonly { container: ViewContainer; location: ViewContainerLocation }[], removed: readonly { container: ViewContainer; location: ViewContainerLocation }[]) { @@ -522,6 +532,10 @@ export class ActivitybarPart extends Part implements IPaneCompositeSelectorPart return this.instantiationService.createInstance(AccountsActivityActionViewItem, action as ActivityAction, () => this.compositeBar.getContextMenuActions(), (theme: IColorTheme) => this.getActivitybarItemColors(theme), this.getActivityHoverOptions()); } + if (action.id === 'workbench.actions.profiles') { + return this.instantiationService.createInstance(ProfilesActivityActionViewItem, action as ActivityAction, () => this.compositeBar.getContextMenuActions(), (theme: IColorTheme) => this.getSettingsProfileItemColors(theme), this.getActivityHoverOptions()); + } + throw new Error(`No view item for action '${action.id}'`); }, orientation: ActionsOrientation.VERTICAL, @@ -547,6 +561,10 @@ export class ActivitybarPart extends Part implements IPaneCompositeSelectorPart } this.globalActivityActionBar.push(this.globalActivityAction); + + if (this.profilesVisibilityPreference) { + this.globalActivityActionBar.push(this.profilesActivityAction = new ActivityAction(this.createProfilesActivity())); + } } private toggleAccountsActivity() { @@ -570,6 +588,58 @@ export class ActivitybarPart extends Part implements IPaneCompositeSelectorPart this.updateGlobalActivity(ACCOUNTS_ACTIVITY_ID); } + private toggleProfilesActivityAction() { + if (!!this.profilesActivityAction === this.profilesVisibilityPreference) { + return; + } + if (this.globalActivityActionBar) { + if (this.profilesActivityAction) { + this.globalActivityActionBar.pull(this.globalActivityActionBar.length() - 1); + this.profilesActivityAction = undefined; + } else { + this.globalActivityActionBar.push(this.profilesActivityAction = new ActivityAction(this.createProfilesActivity())); + } + } + } + + private updateProfilesActivityAction() { + if (!!this.profilesActivityAction !== this.profilesVisibilityPreference) { + this.toggleProfilesActivityAction(); + return; + } + if (this.profilesActivityAction) { + const activity = this.createProfilesActivity(); + if ((this.profilesActivityAction.activity).icon === activity.icon) { + this.profilesActivityAction.activity = activity; + } + // the icon has changed, so we need to recreate the action + else if (this.globalActivityActionBar) { + this.globalActivityActionBar.pull(this.globalActivityActionBar.length() - 1); + this.globalActivityActionBar.push(this.profilesActivityAction = new ActivityAction(activity)); + } + } + } + + private createProfilesActivity(): IProfileActivity { + const icon = this.userDataProfileService.currentProfile.shortName ? ThemeIcon.fromString(this.userDataProfileService.currentProfile.shortName) : undefined; + return { + id: 'workbench.actions.profiles', + name: icon ? this.userDataProfileService.currentProfile.name : this.getProfileEntryDisplayName(this.userDataProfileService.currentProfile), + cssClass: icon ? `${ThemeIcon.asClassName(icon)} profile-activity-item` : 'profile-activity-item', + icon: !!icon + }; + } + + private getProfileEntryDisplayName(profile: IUserDataProfile): string { + if (profile.shortName) { + return profile.shortName; + } + if (profile.isTransient) { + return `T${this.userDataProfileService.currentProfile.name.charAt(this.userDataProfileService.currentProfile.name.length - 1)}`; + } + return this.userDataProfileService.currentProfile.name.substring(0, 2).toUpperCase(); + } + private getCompositeActions(compositeId: string): { activityAction: ViewContainerActivityAction; pinnedAction: ToggleCompositePinnedAction } { let compositeActions = this.compositeActions.get(compositeId); if (!compositeActions) { @@ -784,6 +854,14 @@ export class ActivitybarPart extends Part implements IPaneCompositeSelectorPart }; } + private getSettingsProfileItemColors(theme: IColorTheme): ICompositeBarColors { + return { + ...this.getActivitybarItemColors(theme), + activeForegroundColor: theme.getColor(ACTIVITY_BAR_SETTINGS_PROFILE_FOREGROUND), + inactiveForegroundColor: theme.getColor(ACTIVITY_BAR_SETTINGS_PROFILE_FOREGROUND), + }; + } + override layout(width: number, height: number): void { if (!this.layoutService.isVisible(Parts.ACTIVITYBAR_PART)) { return; @@ -850,6 +928,9 @@ export class ActivitybarPart extends Part implements IPaneCompositeSelectorPart if (e.key === AccountsActivityActionViewItem.ACCOUNTS_VISIBILITY_PREFERENCE_KEY && e.scope === StorageScope.PROFILE) { this.toggleAccountsActivity(); } + if (e.key === ProfilesActivityActionViewItem.PROFILES_VISIBILITY_PREFERENCE_KEY && e.scope === StorageScope.PROFILE) { + this.toggleProfilesActivityAction(); + } } private saveCachedViewContainers(): void { @@ -991,6 +1072,14 @@ export class ActivitybarPart extends Part implements IPaneCompositeSelectorPart this.storageService.store(AccountsActivityActionViewItem.ACCOUNTS_VISIBILITY_PREFERENCE_KEY, value, StorageScope.PROFILE, StorageTarget.USER); } + private get profilesVisibilityPreference(): boolean { + return this.userDataProfilesService.profiles.length > 1 && !this.userDataProfileService.currentProfile.isDefault && this.storageService.getBoolean(ProfilesActivityActionViewItem.PROFILES_VISIBILITY_PREFERENCE_KEY, StorageScope.PROFILE, true); + } + + private set profilesVisibilityPreference(value: boolean) { + this.storageService.store(ProfilesActivityActionViewItem.PROFILES_VISIBILITY_PREFERENCE_KEY, value, StorageScope.PROFILE, StorageTarget.USER); + } + toJSON(): object { return { type: Parts.ACTIVITYBAR_PART diff --git a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css index 453b2041417..292c36545bc 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css +++ b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css @@ -176,6 +176,17 @@ vertical-align: baseline; } +.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-label.profile-activity-item { + height: 20px; + width: 28px; + margin: 9px; + padding: 0px; + justify-content: center; + align-items: center; + font-size: 12px; + line-height: 16px; + border: 1.5px solid; +} /* Right aligned */ diff --git a/src/vs/workbench/browser/parts/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositeBarActions.ts index db28f416db6..7735b136d9e 100644 --- a/src/vs/workbench/browser/parts/compositeBarActions.ts +++ b/src/vs/workbench/browser/parts/compositeBarActions.ts @@ -368,7 +368,7 @@ export class ActivityActionViewItem extends BaseActionViewItem { }); } - private computeTitle(): string { + protected computeTitle(): string { this.keybindingLabel = this.computeKeybindingLabel(); let title = this.keybindingLabel ? localize('titleKeybinding', "{0} ({1})", this.activity.name, this.keybindingLabel) : this.activity.name; const badge = (this.action as ActivityAction).getBadge(); diff --git a/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts b/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts index 1df91396be7..8986a0879ac 100644 --- a/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts @@ -3,18 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { reset } from 'vs/base/browser/dom'; +import { EventLike, reset } from 'vs/base/browser/dom'; +import { BaseActionViewItem, IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { IAction } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { assertType } from 'vs/base/common/types'; import { localize } from 'vs/nls'; -import { createActionViewItem, MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { HiddenItemStrategy, MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; -import { Action2, MenuId, MenuItemAction, registerAction2 } from 'vs/platform/actions/common/actions'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import * as colors from 'vs/platform/theme/common/colorRegistry'; @@ -50,32 +52,50 @@ export class CommandCenterControl { if (action instanceof MenuItemAction && action.id === 'workbench.action.quickOpen') { - class InputLikeViewItem extends MenuEntryActionViewItem { + class CommandCenterViewItem extends BaseActionViewItem { - private readonly workspaceTitle = document.createElement('span'); + constructor(action: IAction, options: IBaseActionViewItemOptions) { + super(undefined, action, options); + } override render(container: HTMLElement): void { super.render(container); - container.classList.add('quickopen', 'left'); + container.classList.add('command-center'); - assertType(this.label); - this.label.classList.add('search'); + const left = document.createElement('span'); + left.classList.add('left'); + // icon (search) const searchIcon = renderIcon(Codicon.search); searchIcon.classList.add('search-icon'); - this.workspaceTitle.classList.add('search-label'); - this.updateTooltip(); - reset(this.label, searchIcon, this.workspaceTitle); - // this._renderAllQuickPickItem(container); + // label: just workspace name and optional decorations + const label = this._getLabel(); + const labelElement = document.createElement('span'); + labelElement.innerText = label; + reset(left, searchIcon, labelElement); - this._store.add(windowTitle.onDidChange(this.updateTooltip, this)); + // icon (dropdown) + const right = document.createElement('span'); + right.classList.add('right'); + const dropIcon = renderIcon(Codicon.chevronDown); + reset(right, dropIcon); + reset(container, left, right); + + // hovers + this._store.add(setupCustomHover(hoverDelegate, right, localize('all', "Show Search Modes..."))); + const leftHover = this._store.add(setupCustomHover(hoverDelegate, left, this.getTooltip())); + + // update label & tooltip when window title changes + this._store.add(windowTitle.onDidChange(() => { + leftHover.update(this.getTooltip()); + labelElement.innerText = this._getLabel(); + })); } - override getTooltip() { - // label: just workspace name and optional decorations + private _getLabel(): string { const { prefix, suffix } = windowTitle.getTitleDecorations(); - let label = windowTitle.workspaceName; + let label = windowTitle.isCustomTitleFormat() ? windowTitle.getWindowTitle() : windowTitle.workspaceName; if (!label) { label = localize('label.dfl', "Search"); } @@ -85,7 +105,10 @@ export class CommandCenterControl { if (suffix) { label = localize('label2', "{0} {1}", label, suffix); } - this.workspaceTitle.innerText = label; + return label; + } + + override getTooltip() { // tooltip: full windowTitle const kb = keybindingService.lookupKeybinding(action.id)?.getLabel(); @@ -95,19 +118,25 @@ export class CommandCenterControl { return title; } - } - return instantiationService.createInstance(InputLikeViewItem, action, { hoverDelegate }); - } else if (action instanceof MenuItemAction && action.id === 'commandCenter.help') { + override onClick(event: EventLike, preserveFocus = false): void { - class ExtraClass extends MenuEntryActionViewItem { - override render(container: HTMLElement): void { - super.render(container); - container.classList.add('quickopen', 'right'); + if (event instanceof MouseEvent) { + let el = event.target; + while (el instanceof HTMLElement) { + if (el.classList.contains('right')) { + quickInputService.quickAccess.show('?'); + return; + } + el = el.parentElement; + } + } + + super.onClick(event, preserveFocus); } } - return instantiationService.createInstance(ExtraClass, action, { hoverDelegate }); + return instantiationService.createInstance(CommandCenterViewItem, action, {}); } else { return createActionViewItem(instantiationService, action, { hoverDelegate }); @@ -130,20 +159,6 @@ export class CommandCenterControl { } } -registerAction2(class extends Action2 { - - constructor() { - super({ - id: 'commandCenter.help', - title: localize('all', "Show Search Modes..."), - icon: Codicon.chevronDown, - menu: { id: MenuId.CommandCenter, order: 101 } - }); - } - run(accessor: ServicesAccessor): void { - accessor.get(IQuickInputService).quickAccess.show('?'); - } -}); // --- theme colors diff --git a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index 5f3f76d376d..654b1fc7eb7 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css @@ -88,10 +88,14 @@ /* Window Title Menu */ .monaco-workbench .part.titlebar>.titlebar-container>.window-title>.command-center { z-index: 2500; - -webkit-app-region: no-drag; padding: 0 8px; } +/* MAC supports click event despite `drag` and therefore we don't need to clear it */ +.monaco-workbench:not(.mac) .part.titlebar>.titlebar-container>.window-title>.command-center { + -webkit-app-region: no-drag; +} + .monaco-workbench .part.titlebar>.titlebar-container>.window-title>.command-center.hide { display: none; } @@ -111,12 +115,23 @@ background-color: var(--vscode-commandCenter-activeBackground); } -.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.command-center .action-item.quickopen.left { - /* border,margin tricks */ - margin-left: 6px; +.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.command-center .action-item.command-center { + display: flex; + align-items: stretch; + color: var(--vscode-commandCenter-foreground); + background-color: var(--vscode-commandCenter-background); + border: 1px solid var(--vscode-commandCenter-border); + overflow: hidden; + margin: 0 6px; border-top-left-radius: 6px; border-bottom-left-radius: 6px; - border-right: none; + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; +} + +.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.command-center .action-item.command-center .left { + display: inline-flex; + justify-content: center; /* width */ width: 38vw; @@ -124,41 +139,30 @@ min-width: 32px; } -.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.command-center .action-item.quickopen.right { - /* border,margin tricks */ - margin-right: 6px; - border-top-right-radius: 6px; - border-bottom-right-radius: 6px; - border-left: none; - - /* width */ - width: 16px; - flex-shrink: 0; -} - -.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.command-center .action-item.quickopen>.action-label { - height: 22px; - line-height: 22px; - padding: 0; - background-color: transparent; - display: inline-flex; - text-align: center; - font-size: 12px; - justify-content: center; - width: 100%; -} - -.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.command-center .action-item.quickopen.left>.action-label.search>.search-icon { +.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.command-center .action-item.command-center .left .search-icon { font-size: 14px; opacity: .8; margin: auto 3px; } -.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.command-center .action-item.quickopen.left>.action-label.search>.search-label { +.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.command-center .action-item.command-center .left .search-label { overflow: hidden; text-overflow: ellipsis; } +.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.command-center .action-item.command-center .right { + margin-left: auto; + padding: 2px 3px 0 0; + width: 16px; + flex-shrink: 0; +} + +.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.command-center .action-item.command-center .left:HOVER, +.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.command-center .action-item.command-center .right:HOVER { + color: var(--vscode-commandCenter-activeForeground); + background-color: var(--vscode-commandCenter-activeBackground); +} + /* Menubar */ .monaco-workbench .part.titlebar>.titlebar-container>.menubar { /* move menubar above drag region as negative z-index on drag region cause greyscale AA */ diff --git a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts index cff3e83c064..7930effaae7 100644 --- a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts @@ -708,8 +708,8 @@ export class CustomMenubarControl extends MenubarControl { if (action instanceof SubmenuItemAction) { let submenu = this.menus[action.item.submenu.id]; if (!submenu) { - submenu = this._register(this.menus[action.item.submenu.id] = this.menuService.createMenu(action.item.submenu, this.contextKeyService)); - this._register(submenu.onDidChange(() => { + submenu = this.mainMenuDisposables.add(this.menus[action.item.submenu.id] = this.menuService.createMenu(action.item.submenu, this.contextKeyService)); + this.mainMenuDisposables.add(submenu.onDidChange(() => { if (!this.focusInsideMenubar) { const actions: IAction[] = []; updateActions(menu, actions, topLevelTitle); diff --git a/src/vs/workbench/browser/parts/titlebar/windowTitle.ts b/src/vs/workbench/browser/parts/titlebar/windowTitle.ts index 9e9922b840a..d6e0cd2a935 100644 --- a/src/vs/workbench/browser/parts/titlebar/windowTitle.ts +++ b/src/vs/workbench/browser/parts/titlebar/windowTitle.ts @@ -25,6 +25,11 @@ import { Schemas } from 'vs/base/common/network'; import { withNullAsUndefined } from 'vs/base/common/types'; import { getVirtualWorkspaceLocation } from 'vs/platform/workspace/common/virtualWorkspace'; +const enum WindowSettingNames { + titleSeparator = 'window.titleSeparator', + title = 'window.title', +} + export class WindowTitle extends Disposable { private static readonly NLS_USER_IS_ADMIN = isWindows ? localize('userIsAdmin', "[Administrator]") : localize('userIsSudo', "[Superuser]"); @@ -71,7 +76,7 @@ export class WindowTitle extends Disposable { } private onConfigurationChanged(event: IConfigurationChangeEvent): void { - if (event.affectsConfiguration('window.title') || event.affectsConfiguration('window.titleSeparator')) { + if (event.affectsConfiguration(WindowSettingNames.title) || event.affectsConfiguration(WindowSettingNames.titleSeparator)) { this.titleUpdater.schedule(); } } @@ -93,7 +98,7 @@ export class WindowTitle extends Disposable { } private doUpdateTitle(): void { - const title = this.getWindowTitle(); + const title = this.getFullWindowTitle(); if (title !== this.title) { // Always set the native window title to identify us properly to the OS let nativeTitle = title; @@ -106,8 +111,8 @@ export class WindowTitle extends Disposable { } } - private getWindowTitle(): string { - let title = this.doGetWindowTitle() || this.productService.nameLong; + private getFullWindowTitle(): string { + let title = this.getWindowTitle() || this.productService.nameLong; const { prefix, suffix } = this.getTitleDecorations(); if (prefix) { title = `${prefix} ${title}`; @@ -171,7 +176,7 @@ export class WindowTitle extends Disposable { * {dirty}: indicator * {separator}: conditional separator */ - private doGetWindowTitle(): string { + getWindowTitle(): string { const editor = this.editorService.activeEditor; const workspace = this.contextService.getWorkspace(); @@ -226,8 +231,8 @@ export class WindowTitle extends Disposable { const folderPath = folder ? this.labelService.getUriLabel(folder.uri) : ''; const dirty = editor?.isDirty() && !editor.isSaving() ? WindowTitle.TITLE_DIRTY : ''; const appName = this.productService.nameLong; - const separator = this.configurationService.getValue('window.titleSeparator'); - const titleTemplate = this.configurationService.getValue('window.title'); + const separator = this.configurationService.getValue(WindowSettingNames.titleSeparator); + const titleTemplate = this.configurationService.getValue(WindowSettingNames.title); return template(titleTemplate, { activeEditorShort, @@ -246,4 +251,10 @@ export class WindowTitle extends Disposable { separator: { label: separator } }); } + + isCustomTitleFormat(): boolean { + const title = this.configurationService.inspect(WindowSettingNames.title); + const titleSeparator = this.configurationService.inspect(WindowSettingNames.titleSeparator); + return title.value !== title.defaultValue || titleSeparator.value !== titleSeparator.defaultValue; + } } diff --git a/src/vs/workbench/browser/checkbox.ts b/src/vs/workbench/browser/parts/views/checkbox.ts similarity index 100% rename from src/vs/workbench/browser/checkbox.ts rename to src/vs/workbench/browser/parts/views/checkbox.ts diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 30b8f3772cc..c4ef9dc916c 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -66,7 +66,7 @@ import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; import { ITreeViewsService } from 'vs/workbench/services/views/browser/treeViewsService'; import { CodeDataTransfers } from 'vs/platform/dnd/browser/dnd'; import { addExternalEditorsDropData, toVSDataTransfer } from 'vs/editor/browser/dnd'; -import { CheckboxStateHandler, TreeItemCheckbox } from 'vs/workbench/browser/checkbox'; +import { CheckboxStateHandler, TreeItemCheckbox } from 'vs/workbench/browser/parts/views/checkbox'; export class TreeViewPane extends ViewPane { @@ -589,6 +589,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { private create() { this.domNode = DOM.$('.tree-explorer-viewlet-tree-view'); this.messageElement = DOM.append(this.domNode, DOM.$('.message')); + this.updateMessage(); this.treeContainer = DOM.append(this.domNode, DOM.$('.customview-tree')); this.treeContainer.classList.add('file-icon-themable-tree', 'show-file-icons'); const focusTracker = this._register(DOM.trackFocus(this.domNode)); diff --git a/src/vs/workbench/browser/web.api.ts b/src/vs/workbench/browser/web.api.ts index bb99447347d..6b8360ae153 100644 --- a/src/vs/workbench/browser/web.api.ts +++ b/src/vs/workbench/browser/web.api.ts @@ -15,9 +15,10 @@ import type { IProductConfiguration } from 'vs/base/common/product'; import type { ICredentialsProvider } from 'vs/platform/credentials/common/credentials'; import type { TunnelProviderFeatures } from 'vs/platform/tunnel/common/tunnel'; import type { IProgress, IProgressCompositeOptions, IProgressDialogOptions, IProgressNotificationOptions, IProgressOptions, IProgressStep, IProgressWindowOptions } from 'vs/platform/progress/common/progress'; -import { IObservableValue } from 'vs/base/common/observableValue'; -import { TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; -import { IEditorOptions } from 'vs/platform/editor/common/editor'; +import type { IObservableValue } from 'vs/base/common/observableValue'; +import type { TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; +import type { ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import type { EditorGroupLayout } from 'vs/workbench/services/editor/common/editorGroupsService'; /** * The `IWorkbench` interface is the API facade for web embedders @@ -30,12 +31,12 @@ export interface IWorkbench { commands: { /** - * Allows to execute any command if known with the provided arguments. - * - * @param command Identifier of the command to execute. - * @param rest Parameters passed to the command function. - * @return A promise that resolves to the returned value of the given command. - */ + * Allows to execute any command if known with the provided arguments. + * + * @param command Identifier of the command to execute. + * @param rest Parameters passed to the command function. + * @return A promise that resolves to the returned value of the given command. + */ executeCommand(command: string, ...args: any[]): Promise; }; @@ -99,6 +100,7 @@ export interface IWorkbench { }; workspace: { + /** * Forwards a port. If the current embedder implements a tunnelFactory then that will be used to make the tunnel. * By default, openTunnel only support localhost; however, a tunnelFactory can be used to support other ips. @@ -107,7 +109,7 @@ export interface IWorkbench { * * @param tunnelOptions The `localPort` is a suggestion only. If that port is not available another will be chosen. */ - openTunnel(tunnelOptions: ITunnelOptions): Thenable; + openTunnel(tunnelOptions: ITunnelOptions): Promise; }; /** @@ -253,7 +255,9 @@ export interface IWorkbenchConstructionOptions { readonly commands?: readonly ICommand[]; /** - * Optional default layout to apply on first time the workspace is opened (unless `force` is specified). + * Optional default layout to apply on first time the workspace is opened + * (unless `force` is specified). This includes visibility of views and + * editors including editor grid layout. */ readonly defaultLayout?: IDefaultLayout; @@ -587,48 +591,67 @@ export interface IInitialColorTheme { } export interface IDefaultView { + + /** + * The identifier of the view to show by default. + */ readonly id: string; } -/** - * @deprecated use `IDefaultEditor.options` instead - */ -export interface IPosition { - readonly line: number; - readonly column: number; -} - -/** - * @deprecated use `IDefaultEditor.options` instead - */ -export interface IRange { - readonly start: IPosition; - readonly end: IPosition; -} - export interface IDefaultEditor { + /** + * The location of the editor in the editor grid layout. + * Editors are layed out in editor groups and the view + * column is counted from top left to bottom right in + * the order of appearance beginning with `1`. + * + * If not provided, the editor will open in the active + * group. + */ + readonly viewColumn?: number; + + /** + * The resource of the editor to open. + */ readonly uri: UriComponents; - readonly options?: IEditorOptions; + /** + * Optional extra options like which editor + * to use or which text to select. + */ + readonly options?: ITextEditorOptions; + + /** + * Will not open an untitled editor in case + * the resource does not exist. + */ readonly openOnlyIfExists?: boolean; - - /** - * @deprecated use `options` instead - */ - readonly selection?: IRange; - - /** - * @deprecated use `options.override` instead - */ - readonly openWith?: string; } export interface IDefaultLayout { + /** + * A list of views to show by default. + */ readonly views?: IDefaultView[]; + + /** + * A list of editors to show by default. + */ readonly editors?: IDefaultEditor[]; + /** + * The layout to use for the workbench. + */ + readonly layout?: { + + /** + * The layout of the editor area. + */ + readonly editors?: EditorGroupLayout; + }; + /** * Forces this layout to be applied even if this isn't * the first time the workspace has been opened diff --git a/src/vs/workbench/browser/web.factory.ts b/src/vs/workbench/browser/web.factory.ts index eef12507e51..f7c6567b9cf 100644 --- a/src/vs/workbench/browser/web.factory.ts +++ b/src/vs/workbench/browser/web.factory.ts @@ -134,8 +134,10 @@ export namespace env { return workbench.env.openUri(target); } - export const telemetryLevel: Promise> = - workbenchPromise.p.then(workbench => workbench.env.telemetryLevel); + /** + * {@linkcode IWorkbench.env IWorkbench.env.telemetryLevel} + */ + export const telemetryLevel: Promise> = workbenchPromise.p.then(workbench => workbench.env.telemetryLevel); } export namespace window { @@ -160,6 +162,7 @@ export namespace workspace { */ export async function openTunnel(tunnelOptions: ITunnelOptions): Promise { const workbench = await workbenchPromise.p; + return workbench.workspace.openTunnel(tunnelOptions); } } diff --git a/src/vs/workbench/browser/webview.ts b/src/vs/workbench/browser/webview.ts index 27ad38106b7..cd6bfa2be82 100644 --- a/src/vs/workbench/browser/webview.ts +++ b/src/vs/workbench/browser/webview.ts @@ -8,8 +8,9 @@ export async function parentOriginHash(parentOrigin: string, salt: string): Promise { // This same code is also inlined at `src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html` if (!crypto.subtle) { - throw new Error(`Can't compute sha-256`); + throw new Error(`'crypto.subtle' is not available so webviews will not work. This is likely because the editor is not running in a secure context (https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts).`); } + const strData = JSON.stringify({ parentOrigin, salt }); const encoder = new TextEncoder(); const arrData = encoder.encode(strData); diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 21352f7fa9c..a219cd7ed2e 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -19,7 +19,6 @@ import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsSe import { ICompositeControl, IComposite } from 'vs/workbench/common/composite'; import { FileType, IFileService } from 'vs/platform/files/common/files'; import { IPathData } from 'vs/platform/window/common/window'; -import { coalesce } from 'vs/base/common/arrays'; import { IExtUri } from 'vs/base/common/resources'; import { Schemas } from 'vs/base/common/network'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -1382,20 +1381,20 @@ class EditorFactoryRegistry implements IEditorFactoryRegistry { Registry.add(EditorExtensions.EditorFactory, new EditorFactoryRegistry()); -export async function pathsToEditors(paths: IPathData[] | undefined, fileService: IFileService): Promise<(IResourceEditorInput | IUntitledTextResourceEditorInput)[]> { +export async function pathsToEditors(paths: IPathData[] | undefined, fileService: IFileService): Promise> { if (!paths || !paths.length) { return []; } - const editors = await Promise.all(paths.map(async path => { + return await Promise.all(paths.map(async path => { const resource = URI.revive(path.fileUri); if (!resource) { - return; + return undefined; } const canHandleResource = await fileService.canHandleResource(resource); if (!canHandleResource) { - return; + return undefined; } let exists = path.exists; @@ -1410,11 +1409,11 @@ export async function pathsToEditors(paths: IPathData[] | undefined, fileService } if (!exists && path.openOnlyIfExists) { - return; + return undefined; } if (type === FileType.Directory) { - return; + return undefined; } const options: IEditorOptions = { @@ -1422,17 +1421,12 @@ export async function pathsToEditors(paths: IPathData[] | undefined, fileService pinned: true }; - let input: IResourceEditorInput | IUntitledTextResourceEditorInput; if (!exists) { - input = { resource, options, forceUntitled: true }; - } else { - input = { resource, options }; + return { resource, options, forceUntitled: true }; } - return input; + return { resource, options }; })); - - return coalesce(editors); } export const enum EditorsOrder { diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index 4c7a41c2fcb..b89e7d795d6 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -625,6 +625,19 @@ export const ACTIVITY_BAR_BADGE_FOREGROUND = registerColor('activityBarBadge.for hcLight: Color.white }, localize('activityBarBadgeForeground', "Activity notification badge foreground color. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); +export const ACTIVITY_BAR_SETTINGS_PROFILE_FOREGROUND = registerColor('activityBarItem.settingsProfilesForeground', { + dark: ACTIVITY_BAR_INACTIVE_FOREGROUND, + light: ACTIVITY_BAR_INACTIVE_FOREGROUND, + hcDark: ACTIVITY_BAR_INACTIVE_FOREGROUND, + hcLight: ACTIVITY_BAR_INACTIVE_FOREGROUND +}, localize('statusBarItemSettingsProfileForeground', "Foreground color for the settings profile entry on the activity bar.")); + +export const ACTIVITY_BAR_SETTINGS_PROFILE_HOVER_FOREGROUND = registerColor('activityBarItem.settingsProfilesHoverForeground', { + dark: ACTIVITY_BAR_FOREGROUND, + light: ACTIVITY_BAR_FOREGROUND, + hcDark: ACTIVITY_BAR_FOREGROUND, + hcLight: ACTIVITY_BAR_FOREGROUND +}, localize('activityBarItem.settingsProfilesHoverForeground', "Foreground color for the settings profile entry on the activity bar when hovering.")); // < --- Remote --- > diff --git a/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts b/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts index 0aa32588d28..75a3c29ecd7 100644 --- a/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts @@ -36,6 +36,7 @@ export class CommentGlyphWidget { color: themeColorFromId(overviewRulerCommentingRangeForeground), position: OverviewRulerLane.Center }, + collapseOnReplaceEdit: true, linesDecorationsClassName: `comment-range-glyph comment-thread` }; diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index 830a95a92d2..b900d70c0d9 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -15,7 +15,7 @@ import { IModelDecorationOptions, TrackedRangeStickiness, ITextModel, OverviewRu import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IDebugService, IBreakpoint, CONTEXT_BREAKPOINT_WIDGET_VISIBLE, BreakpointWidgetContext, IBreakpointEditorContribution, IBreakpointUpdateData, IDebugConfiguration, State, IDebugSession, DebuggerUiMessage } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, IBreakpoint, CONTEXT_BREAKPOINT_WIDGET_VISIBLE, BreakpointWidgetContext, IBreakpointEditorContribution, IBreakpointUpdateData, IDebugConfiguration, State, IDebugSession, DebuggerString } from 'vs/workbench/contrib/debug/common/debug'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { BreakpointWidget } from 'vs/workbench/contrib/debug/browser/breakpointWidget'; import { IDisposable, dispose, disposeIfDisposable } from 'vs/base/common/lifecycle'; @@ -86,7 +86,7 @@ function getBreakpointDecorationOptions(accessor: ServicesAccessor, model: IText let langId: string | undefined; unverifiedMessage = debugService.getModel().getSessions().map(s => { const dbg = debugService.getAdapterManager().getDebugger(s.configuration.type); - const message = dbg?.uiMessages?.[DebuggerUiMessage.UnverifiedBreakpoints]; + const message = dbg?.strings?.[DebuggerString.UnverifiedBreakpoints]; if (message) { if (!langId) { // Lazily compute this, only if needed for some debug adapter diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index 58e653531b8..414b1b2494e 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -45,7 +45,7 @@ import { IEditorPane } from 'vs/workbench/common/editor'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; import { DisassemblyView } from 'vs/workbench/contrib/debug/browser/disassemblyView'; -import { BREAKPOINTS_VIEW_ID, BREAKPOINT_EDITOR_CONTRIBUTION_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_BREAKPOINT_INPUT_FOCUSED, CONTEXT_BREAKPOINT_ITEM_TYPE, CONTEXT_BREAKPOINT_SUPPORTS_CONDITION, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_IN_DEBUG_MODE, DebuggerUiMessage, DEBUG_SCHEME, IBaseBreakpoint, IBreakpoint, IBreakpointEditorContribution, IDataBreakpoint, IDebugModel, IDebugService, IEnablement, IExceptionBreakpoint, IFunctionBreakpoint, IInstructionBreakpoint, State } from 'vs/workbench/contrib/debug/common/debug'; +import { BREAKPOINTS_VIEW_ID, BREAKPOINT_EDITOR_CONTRIBUTION_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_BREAKPOINT_INPUT_FOCUSED, CONTEXT_BREAKPOINT_ITEM_TYPE, CONTEXT_BREAKPOINT_SUPPORTS_CONDITION, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_IN_DEBUG_MODE, DebuggerString, DEBUG_SCHEME, IBaseBreakpoint, IBreakpoint, IBreakpointEditorContribution, IDataBreakpoint, IDebugModel, IDebugService, IEnablement, IExceptionBreakpoint, IFunctionBreakpoint, IInstructionBreakpoint, State } from 'vs/workbench/contrib/debug/common/debug'; import { Breakpoint, DataBreakpoint, ExceptionBreakpoint, FunctionBreakpoint, InstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassemblyViewInput'; import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; @@ -284,7 +284,7 @@ export class BreakpointsView extends ViewPane { const currentType = this.debugService.getViewModel().focusedSession?.configuration.type; const dbg = currentType ? this.debugService.getAdapterManager().getDebugger(currentType) : undefined; - const message = dbg?.uiMessages && dbg.uiMessages[DebuggerUiMessage.UnverifiedBreakpoints]; + const message = dbg?.strings?.[DebuggerString.UnverifiedBreakpoints]; const debuggerHasUnverifiedBps = message && this.debugService.getModel().getBreakpoints().filter(bp => { if (bp.verified || !bp.enabled) { return false; diff --git a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts index 45e0971605f..3c2568989db 100644 --- a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts @@ -164,9 +164,9 @@ export class ConfigurationManager implements IConfigurationManager { } async getDynamicProviders(): Promise<{ label: string; type: string; getProvider: () => Promise; pick: () => Promise<{ launch: ILaunch; config: IConfig } | undefined> }[]> { - const extensions = await this.extensionService.getExtensions(); + await this.extensionService.whenInstalledExtensionsRegistered(); const onDebugDynamicConfigurationsName = 'onDebugDynamicConfigurations'; - const debugDynamicExtensionsTypes = extensions.reduce((acc, e) => { + const debugDynamicExtensionsTypes = this.extensionService.extensions.reduce((acc, e) => { if (!e.activationEvents) { return acc; } diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index dbd8289acf0..11c3f051a43 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -163,7 +163,7 @@ export interface IDebugger { export interface IDebuggerMetadata { label: string; type: string; - uiMessages?: { [key in DebuggerUiMessage]: string }; + strings?: { [key in DebuggerString]: string }; interestedInLanguage(languageId: string): boolean; } @@ -786,7 +786,7 @@ export interface IDebuggerContribution extends IPlatformSpecificAdapterContribut variables?: { [key: string]: string }; when?: string; deprecated?: string; - uiMessages?: { [key in DebuggerUiMessage]: string }; + strings?: { [key in DebuggerString]: string }; } export interface IBreakpointContribution { @@ -862,7 +862,7 @@ export interface IConfigurationManager { resolveConfigurationByProviders(folderUri: uri | undefined, type: string | undefined, debugConfiguration: any, token: CancellationToken): Promise; } -export enum DebuggerUiMessage { +export enum DebuggerString { UnverifiedBreakpoints = 'unverifiedBreakpoints' } diff --git a/src/vs/workbench/contrib/debug/common/debugSchemas.ts b/src/vs/workbench/contrib/debug/common/debugSchemas.ts index 26575ff3b88..7363af0723c 100644 --- a/src/vs/workbench/contrib/debug/common/debugSchemas.ts +++ b/src/vs/workbench/contrib/debug/common/debugSchemas.ts @@ -106,6 +106,16 @@ export const debuggersExtPoint = extensionsRegistry.ExtensionsRegistry.registerE type: 'string' } } + }, + strings: { + description: nls.localize('vscode.extension.contributes.debuggers.strings', "UI strings contributed by this debug adapter."), + type: 'object', + properties: { + unverifiedBreakpoints: { + description: nls.localize('vscode.extension.contributes.debuggers.strings.unverifiedBreakpoints', "When there are unverified breakpoints in a language supported by this debug adapter, this message will appear on the breakpoint hover and in the breakpoints view. Markdown and command links are supported."), + type: 'string' + } + } } } } diff --git a/src/vs/workbench/contrib/debug/common/debugger.ts b/src/vs/workbench/contrib/debug/common/debugger.ts index f7041e2099a..4cdb2d5c41a 100644 --- a/src/vs/workbench/contrib/debug/common/debugger.ts +++ b/src/vs/workbench/contrib/debug/common/debugger.ts @@ -147,8 +147,8 @@ export class Debugger implements IDebugger, IDebuggerMetadata { return !this.debuggerWhen || this.contextKeyService.contextMatchesRules(this.debuggerWhen); } - get uiMessages() { - return this.debuggerContribution.uiMessages; + get strings() { + return this.debuggerContribution.strings ?? (this.debuggerContribution as any).uiMessages; } interestedInLanguage(languageId: string): boolean { diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts b/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts index 9136ed15e08..fdce08d7082 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts @@ -26,6 +26,7 @@ import { getCurrentAuthenticationSessionInfo } from 'vs/workbench/services/authe import { isWeb } from 'vs/base/common/platform'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { Codicon } from 'vs/base/common/codicons'; +import { IUserDataSyncMachinesService, UserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines'; type ExistingSession = IQuickPickItem & { session: AuthenticationSession & { providerId: string } }; type AuthenticationProviderOption = IQuickPickItem & { provider: IAuthenticationProvider }; @@ -36,7 +37,8 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes _serviceBrand = undefined; private serverConfiguration = this.productService['editSessions.store']; - private storeClient: UserDataSyncStoreClient | undefined; + private storeClient: EditSessionsStoreClient | undefined; + private machineClient: IUserDataSyncMachinesService | undefined; #authenticationInfo: { sessionId: string; token: string; providerId: string } | undefined; private static CACHED_SESSION_STORAGE_KEY = 'editSessionAccountPreference'; @@ -89,6 +91,10 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes throw new Error('Please sign in to store your edit session.'); } + if (editSession.machine === undefined) { + editSession.machine = await this.getOrCreateCurrentMachineId(); + } + return this.storeClient!.writeResource('editSessions', JSON.stringify(editSession), null, undefined, createSyncHeaders(generateUuid())); } @@ -175,13 +181,17 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes } if (!this.storeClient) { - this.storeClient = new UserDataSyncStoreClient(URI.parse(this.serverConfiguration.url), this.productService, this.requestService, this.logService, this.environmentService, this.fileService, this.storageService); + this.storeClient = new EditSessionsStoreClient(URI.parse(this.serverConfiguration.url), this.productService, this.requestService, this.logService, this.environmentService, this.fileService, this.storageService); this._register(this.storeClient.onTokenFailed(() => { this.logService.info('Clearing edit sessions authentication preference because of successive token failures.'); this.clearAuthenticationPreference(); })); } + if (this.machineClient === undefined) { + this.machineClient = new UserDataSyncMachinesService(this.environmentService, this.fileService, this.storageService, this.storeClient!, this.logService, this.productService); + } + // If we already have an existing auth session in memory, use that if (this.#authenticationInfo !== undefined) { return true; @@ -196,6 +206,30 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes return authenticationSession !== undefined; } + private cachedMachines: Map | undefined; + + async getMachineById(machineId: string) { + await this.initialize(false); + + if (!this.cachedMachines) { + const machines = await this.machineClient!.getMachines(); + this.cachedMachines = machines.reduce((map, machine) => map.set(machine.id, machine.name), new Map()); + } + + return this.cachedMachines.get(machineId); + } + + private async getOrCreateCurrentMachineId(): Promise { + const currentMachineId = await this.machineClient!.getMachines().then((machines) => machines.find((m) => m.isCurrent)?.id); + + if (currentMachineId === undefined) { + await this.machineClient!.addCurrentMachine(); + return await this.machineClient!.getMachines().then((machines) => machines.find((m) => m.isCurrent)!.id); + } + + return currentMachineId; + } + private async getAuthenticationSession(fromContinueOn: boolean) { // If the user signed in previously and the session is still available, reuse that without prompting the user again if (this.existingSessionId) { @@ -461,3 +495,7 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes })); } } + +class EditSessionsStoreClient extends UserDataSyncStoreClient { + _serviceBrand: any; +} diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessionsViews.ts b/src/vs/workbench/contrib/editSessions/browser/editSessionsViews.ts index dc1b524784b..feba1d9df63 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessionsViews.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessionsViews.ts @@ -201,17 +201,26 @@ class EditSessionDataViewDataProvider implements ITreeViewDataProvider { private async getAllEditSessions(): Promise { const allEditSessions = await this.editSessionsStorageService.list(); this.editSessionsCount.set(allEditSessions.length); - return allEditSessions.map((session) => { + const editSessions = []; + + for (const session of allEditSessions) { const resource = URI.from({ scheme: EDIT_SESSIONS_SCHEME, authority: 'remote-session-content', path: `/${session.ref}` }); - return { + const sessionData = await this.editSessionsStorageService.read(session.ref); + const label = sessionData?.editSession.folders.map((folder) => folder.name).join(', ') ?? session.ref; + const machineId = sessionData?.editSession.machine; + const description = machineId === undefined ? fromNow(session.created, true) : `${fromNow(session.created, true)}\u00a0\u00a0\u2022\u00a0\u00a0${await this.editSessionsStorageService.getMachineById(machineId)}`; + + editSessions.push({ handle: resource.toString(), collapsibleState: TreeItemCollapsibleState.Collapsed, - label: { label: fromNow(session.created, true) }, - description: session.ref, + label: { label }, + description: description, themeIcon: Codicon.repo, contextValue: `edit-session` - }; - }); + }); + } + + return editSessions; } private async getEditSession(ref: string): Promise { @@ -221,6 +230,11 @@ class EditSessionDataViewDataProvider implements ITreeViewDataProvider { return []; } + if (data.editSession.folders.length === 1) { + const folder = data.editSession.folders[0]; + return this.getEditSessionFolderContents(ref, folder.name); + } + return data.editSession.folders.map((folder) => { const resource = URI.from({ scheme: EDIT_SESSIONS_SCHEME, authority: 'remote-session-content', path: `/${data.ref}/${folder.name}` }); return { diff --git a/src/vs/workbench/contrib/editSessions/common/editSessions.ts b/src/vs/workbench/contrib/editSessions/common/editSessions.ts index 99f664e1179..b0afa0d9e63 100644 --- a/src/vs/workbench/contrib/editSessions/common/editSessions.ts +++ b/src/vs/workbench/contrib/editSessions/common/editSessions.ts @@ -29,6 +29,7 @@ export interface IEditSessionsStorageService { write(editSession: EditSession): Promise; delete(ref: string | null): Promise; list(): Promise; + getMachineById(machineId: string): Promise; } export const IEditSessionsLogService = createDecorator('IEditSessionsLogService'); @@ -69,6 +70,7 @@ export const EditSessionSchemaVersion = 2; export interface EditSession { version: number; + machine?: string; folders: Folder[]; } diff --git a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts index c801cfe94f6..e9d58bface0 100644 --- a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts @@ -100,7 +100,8 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { private async _resolveExtensions(): Promise { // We only deal with extensions with source code! - const extensionsDescriptions = (await this._extensionService.getExtensions()).filter((extension) => { + await this._extensionService.whenInstalledExtensionsRegistered(); + const extensionsDescriptions = this._extensionService.extensions.filter((extension) => { return Boolean(extension.main) || Boolean(extension.browser); }); const marketplaceMap: { [id: string]: IExtension } = Object.create(null); diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 6685732eeee..41169964b15 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -544,7 +544,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi when: ContextKeyExpr.and(CONTEXT_HAS_GALLERY, ContextKeyExpr.or(CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER)) }, run: async (accessor: ServicesAccessor) => { - accessor.get(IViewsService).openViewContainer(VIEWLET_ID); + accessor.get(IViewsService).openViewContainer(VIEWLET_ID, true); } }); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 06c7d1cafc0..634dd88be4e 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -1076,7 +1076,7 @@ export class ManageExtensionAction extends ExtensionDropDownAction { this.update(); } - async getActionGroups(runningExtensions: IExtensionDescription[]): Promise { + async getActionGroups(): Promise { const groups: IAction[][] = []; const contextMenuActionsGroups = await getContextMenuActionsGroups(this.extension, this.contextKeyService, this.instantiationService); const themeActions: IAction[] = [], installActions: IAction[] = [], otherActionGroups: IAction[][] = []; @@ -1099,8 +1099,8 @@ export class ManageExtensionAction extends ExtensionDropDownAction { this.instantiationService.createInstance(EnableForWorkspaceAction) ]); groups.push([ - this.instantiationService.createInstance(DisableGloballyAction, runningExtensions), - this.instantiationService.createInstance(DisableForWorkspaceAction, runningExtensions) + this.instantiationService.createInstance(DisableGloballyAction), + this.instantiationService.createInstance(DisableForWorkspaceAction) ]); groups.push([ ...(installActions.length ? installActions : []), @@ -1120,8 +1120,8 @@ export class ManageExtensionAction extends ExtensionDropDownAction { } override async run(): Promise { - const runtimeExtensions = await this.extensionService.getExtensions(); - return super.run({ actionGroups: await this.getActionGroups(runtimeExtensions), disposeActionsOnHide: true }); + await this.extensionService.whenInstalledExtensionsRegistered(); + return super.run({ actionGroups: await this.getActionGroups(), disposeActionsOnHide: true }); } update(): void { @@ -1372,24 +1372,21 @@ export class DisableForWorkspaceAction extends ExtensionAction { static readonly ID = 'extensions.disableForWorkspace'; static readonly LABEL = localize('disableForWorkspaceAction', "Disable (Workspace)"); - constructor(private _runningExtensions: IExtensionDescription[], + constructor( @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, - @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService + @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, + @IExtensionService private readonly extensionService: IExtensionService, ) { super(DisableForWorkspaceAction.ID, DisableForWorkspaceAction.LABEL, ExtensionAction.LABEL_ACTION_CLASS); this.tooltip = localize('disableForWorkspaceActionToolTip', "Disable this extension only in this workspace"); this.update(); - } - - set runningExtensions(runningExtensions: IExtensionDescription[]) { - this._runningExtensions = runningExtensions; - this.update(); + this._register(this.extensionService.onDidChangeExtensions(() => this.update())); } update(): void { this.enabled = false; - if (this.extension && this.extension.local && this._runningExtensions.some(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier) && this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY)) { + if (this.extension && this.extension.local && this.extensionService.extensions.some(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier) && this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY)) { this.enabled = this.extension.state === ExtensionState.Installed && (this.extension.enablementState === EnablementState.EnabledGlobally || this.extension.enablementState === EnablementState.EnabledWorkspace) && this.extensionEnablementService.canChangeWorkspaceEnablement(this.extension.local); @@ -1410,23 +1407,19 @@ export class DisableGloballyAction extends ExtensionAction { static readonly LABEL = localize('disableGloballyAction', "Disable"); constructor( - private _runningExtensions: IExtensionDescription[], @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, - @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService + @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, + @IExtensionService private readonly extensionService: IExtensionService, ) { super(DisableGloballyAction.ID, DisableGloballyAction.LABEL, ExtensionAction.LABEL_ACTION_CLASS); this.tooltip = localize('disableGloballyActionToolTip', "Disable this extension"); this.update(); - } - - set runningExtensions(runningExtensions: IExtensionDescription[]) { - this._runningExtensions = runningExtensions; - this.update(); + this._register(this.extensionService.onDidChangeExtensions(() => this.update())); } update(): void { this.enabled = false; - if (this.extension && this.extension.local && this._runningExtensions.some(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier))) { + if (this.extension && this.extension.local && this.extensionService.extensions.some(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier))) { this.enabled = this.extension.state === ExtensionState.Installed && (this.extension.enablementState === EnablementState.EnabledGlobally || this.extension.enablementState === EnablementState.EnabledWorkspace) && this.extensionEnablementService.canChangeEnablement(this.extension.local); @@ -1458,21 +1451,12 @@ export class EnableDropDownAction extends ActionWithDropDownAction { export class DisableDropDownAction extends ActionWithDropDownAction { constructor( - @IExtensionService extensionService: IExtensionService, @IInstantiationService instantiationService: IInstantiationService ) { - const actions = [ - instantiationService.createInstance(DisableGloballyAction, []), - instantiationService.createInstance(DisableForWorkspaceAction, []) - ]; - super('extensions.disable', localize('disableAction', "Disable"), [actions]); - - const updateRunningExtensions = async () => { - const runningExtensions = await extensionService.getExtensions(); - actions.forEach(a => a.runningExtensions = runningExtensions); - }; - updateRunningExtensions(); - this._register(extensionService.onDidChangeExtensions(() => updateRunningExtensions())); + super('extensions.disable', localize('disableAction', "Disable"), [[ + instantiationService.createInstance(DisableGloballyAction), + instantiationService.createInstance(DisableForWorkspaceAction) + ]]); } } @@ -1483,31 +1467,20 @@ export class ReloadAction extends ExtensionAction { private static readonly DisabledClass = `${ReloadAction.EnabledClass} disabled`; updateWhenCounterExtensionChanges: boolean = true; - private _runningExtensions: IExtensionDescription[] | null = null; constructor( - @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IHostService private readonly hostService: IHostService, @IExtensionService private readonly extensionService: IExtensionService, - @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, - @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, - @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, - @IProductService productService: IProductService, - @IConfigurationService configurationService: IConfigurationService, ) { super('extensions.reload', localize('reloadAction', "Reload"), ReloadAction.DisabledClass, false); - this._register(this.extensionService.onDidChangeExtensions(this.updateRunningExtensions, this)); - this.updateRunningExtensions(); - } - - private updateRunningExtensions(): void { - this.extensionService.getExtensions().then(runningExtensions => { this._runningExtensions = runningExtensions; this.update(); }); + this._register(this.extensionService.onDidChangeExtensions(() => this.update())); + this.update(); } update(): void { this.enabled = false; this.tooltip = ''; - if (!this.extension || !this._runningExtensions) { + if (!this.extension) { return; } const state = this.extension.state; @@ -1517,125 +1490,15 @@ export class ReloadAction extends ExtensionAction { if (this.extension.local && this.extension.local.manifest && this.extension.local.manifest.contributes && this.extension.local.manifest.contributes.localizations && this.extension.local.manifest.contributes.localizations.length > 0) { return; } - this.computeReloadState(); + + const reloadTooltip = this.extension.reloadRequiredStatus; + this.enabled = reloadTooltip !== undefined; + this.label = reloadTooltip !== undefined ? localize('reload required', 'Reload Required') : ''; + this.tooltip = reloadTooltip !== undefined ? reloadTooltip : ''; + this.class = this.enabled ? ReloadAction.EnabledClass : ReloadAction.DisabledClass; } - private computeReloadState(): void { - if (!this._runningExtensions || !this.extension) { - return; - } - - const isUninstalled = this.extension.state === ExtensionState.Uninstalled; - const runningExtension = this._runningExtensions.find(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier)); - - if (isUninstalled) { - const canRemoveRunningExtension = runningExtension && this.extensionService.canRemoveExtension(runningExtension); - const isSameExtensionRunning = runningExtension && (!this.extension.server || this.extension.server === this.extensionManagementServerService.getExtensionManagementServer(toExtension(runningExtension))); - if (!canRemoveRunningExtension && isSameExtensionRunning) { - this.enabled = true; - this.label = localize('reloadRequired', "Reload Required"); - this.tooltip = localize('postUninstallTooltip', "Please reload Visual Studio Code to complete the uninstallation of this extension."); - alert(localize('uninstallExtensionComplete', "Please reload Visual Studio Code to complete the uninstallation of the extension {0}.", this.extension.displayName)); - } - return; - } - if (this.extension.local) { - const isSameExtensionRunning = runningExtension && this.extension.server === this.extensionManagementServerService.getExtensionManagementServer(toExtension(runningExtension)); - const isEnabled = this.extensionEnablementService.isEnabled(this.extension.local); - - // Extension is running - if (runningExtension) { - if (isEnabled) { - // No Reload is required if extension can run without reload - if (this.extensionService.canAddExtension(toExtensionDescription(this.extension.local))) { - return; - } - const runningExtensionServer = this.extensionManagementServerService.getExtensionManagementServer(toExtension(runningExtension)); - - if (isSameExtensionRunning) { - // Different version or target platform of same extension is running. Requires reload to run the current version - if (this.extension.version !== runningExtension.version || this.extension.local.targetPlatform !== runningExtension.targetPlatform) { - this.enabled = true; - this.label = localize('reloadRequired', "Reload Required"); - this.tooltip = localize('postUpdateTooltip', "Please reload Visual Studio Code to enable the updated extension."); - return; - } - - const extensionInOtherServer = this.extensionsWorkbenchService.installed.filter(e => areSameExtensions(e.identifier, this.extension!.identifier) && e.server !== this.extension!.server)[0]; - if (extensionInOtherServer) { - // This extension prefers to run on UI/Local side but is running in remote - if (runningExtensionServer === this.extensionManagementServerService.remoteExtensionManagementServer && this.extensionManifestPropertiesService.prefersExecuteOnUI(this.extension.local!.manifest)) { - this.enabled = true; - this.label = localize('reloadRequired', "Reload Required"); - this.tooltip = localize('enable locally', "Please reload Visual Studio Code to enable this extension locally."); - return; - } - - // This extension prefers to run on Workspace/Remote side but is running in local - if (runningExtensionServer === this.extensionManagementServerService.localExtensionManagementServer && this.extensionManifestPropertiesService.prefersExecuteOnWorkspace(this.extension.local!.manifest)) { - this.enabled = true; - this.label = localize('reloadRequired', "Reload Required"); - this.tooltip = localize('enable remote', "Please reload Visual Studio Code to enable this extension in {0}.", this.extensionManagementServerService.remoteExtensionManagementServer?.label); - return; - } - } - - } else { - - if (this.extension.server === this.extensionManagementServerService.localExtensionManagementServer && runningExtensionServer === this.extensionManagementServerService.remoteExtensionManagementServer) { - // This extension prefers to run on UI/Local side but is running in remote - if (this.extensionManifestPropertiesService.prefersExecuteOnUI(this.extension.local!.manifest)) { - this.enabled = true; - this.label = localize('reloadRequired', "Reload Required"); - this.tooltip = localize('postEnableTooltip', "Please reload Visual Studio Code to enable this extension."); - } - } - if (this.extension.server === this.extensionManagementServerService.remoteExtensionManagementServer && runningExtensionServer === this.extensionManagementServerService.localExtensionManagementServer) { - // This extension prefers to run on Workspace/Remote side but is running in local - if (this.extensionManifestPropertiesService.prefersExecuteOnWorkspace(this.extension.local!.manifest)) { - this.enabled = true; - this.label = localize('reloadRequired', "Reload Required"); - this.tooltip = localize('postEnableTooltip', "Please reload Visual Studio Code to enable this extension."); - } - } - } - return; - } else { - if (isSameExtensionRunning) { - this.enabled = true; - this.label = localize('reloadRequired', "Reload Required"); - this.tooltip = localize('postDisableTooltip', "Please reload Visual Studio Code to disable this extension."); - } - } - return; - } - - // Extension is not running - else { - if (isEnabled && !this.extensionService.canAddExtension(toExtensionDescription(this.extension.local))) { - this.enabled = true; - this.label = localize('reloadRequired', "Reload Required"); - this.tooltip = localize('postEnableTooltip', "Please reload Visual Studio Code to enable this extension."); - return; - } - - const otherServer = this.extension.server ? this.extension.server === this.extensionManagementServerService.localExtensionManagementServer ? this.extensionManagementServerService.remoteExtensionManagementServer : this.extensionManagementServerService.localExtensionManagementServer : null; - if (otherServer && this.extension.enablementState === EnablementState.DisabledByExtensionKind) { - const extensionInOtherServer = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, this.extension!.identifier) && e.server === otherServer)[0]; - // Same extension in other server exists and - if (extensionInOtherServer && extensionInOtherServer.local && this.extensionEnablementService.isEnabled(extensionInOtherServer.local)) { - this.enabled = true; - this.label = localize('reloadRequired', "Reload Required"); - this.tooltip = localize('postEnableTooltip', "Please reload Visual Studio Code to enable this extension."); - alert(localize('installExtensionCompletedAndReloadRequired', "Installing extension {0} is completed. Please reload Visual Studio Code to enable it.", this.extension.displayName)); - return; - } - } - } - } - } - override run(): Promise { return Promise.resolve(this.hostService.reload()); } @@ -2195,14 +2058,12 @@ export class ExtensionStatusLabelAction extends Action implements IExtensionCont } update(): void { - this.computeLabel() - .then(label => { - this.label = label || ''; - this.class = label ? ExtensionStatusLabelAction.ENABLED_CLASS : ExtensionStatusLabelAction.DISABLED_CLASS; - }); + const label = this.computeLabel(); + this.label = label || ''; + this.class = label ? ExtensionStatusLabelAction.ENABLED_CLASS : ExtensionStatusLabelAction.DISABLED_CLASS; } - private async computeLabel(): Promise { + private computeLabel(): string | null { if (!this.extension) { return null; } @@ -2215,9 +2076,8 @@ export class ExtensionStatusLabelAction extends Action implements IExtensionCont } this.enablementState = this.extension.enablementState; - const runningExtensions = await this.extensionService.getExtensions(); const canAddExtension = () => { - const runningExtension = runningExtensions.filter(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier))[0]; + const runningExtension = this.extensionService.extensions.filter(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier))[0]; if (this.extension!.local) { if (runningExtension && this.extension!.version === runningExtension.version) { return true; @@ -2228,7 +2088,7 @@ export class ExtensionStatusLabelAction extends Action implements IExtensionCont }; const canRemoveExtension = () => { if (this.extension!.local) { - if (runningExtensions.every(e => !(areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier) && this.extension!.server === this.extensionManagementServerService.getExtensionManagementServer(toExtension(e))))) { + if (this.extensionService.extensions.every(e => !(areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier) && this.extension!.server === this.extensionManagementServerService.getExtensionManagementServer(toExtension(e))))) { return true; } return this.extensionService.canRemoveExtension(toExtensionDescription(this.extension!.local)); @@ -2314,7 +2174,6 @@ export class ExtensionStatusAction extends ExtensionAction { private static readonly CLASS = `${ExtensionAction.ICON_ACTION_CLASS} extension-status`; updateWhenCounterExtensionChanges: boolean = true; - private _runningExtensions: IExtensionDescription[] | null = null; private _status: ExtensionStatus | undefined; get status(): ExtensionStatus | undefined { return this._status; } @@ -2339,15 +2198,10 @@ export class ExtensionStatusAction extends ExtensionAction { ) { super('extensions.status', '', `${ExtensionStatusAction.CLASS} hide`, false); this._register(this.labelService.onDidChangeFormatters(() => this.update(), this)); - this._register(this.extensionService.onDidChangeExtensions(this.updateRunningExtensions, this)); - this.updateRunningExtensions(); + this._register(this.extensionService.onDidChangeExtensions(() => this.update())); this.update(); } - private updateRunningExtensions(): void { - this.extensionService.getExtensions().then(runningExtensions => { this._runningExtensions = runningExtensions; this.update(); }); - } - update(): void { this.updateThrottler.queue(() => this.computeAndUpdateStatus()); } @@ -2400,7 +2254,6 @@ export class ExtensionStatusAction extends ExtensionAction { if (!this.extension.local || !this.extension.server || - !this._runningExtensions || this.extension.state !== ExtensionState.Installed ) { return; @@ -2501,7 +2354,7 @@ export class ExtensionStatusAction extends ExtensionAction { return; } - const runningExtension = this._runningExtensions.filter(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier))[0]; + const runningExtension = this.extensionService.extensions.filter(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier))[0]; const runningExtensionServer = runningExtension ? this.extensionManagementServerService.getExtensionManagementServer(toExtension(runningExtension)) : null; if (this.extension.server === this.extensionManagementServerService.localExtensionManagementServer && runningExtensionServer === this.extensionManagementServerService.remoteExtensionManagementServer) { if (this.extensionManifestPropertiesService.prefersExecuteOnWorkspace(this.extension.local!.manifest)) { @@ -2532,7 +2385,7 @@ export class ExtensionStatusAction extends ExtensionAction { } const isEnabled = this.workbenchExtensionEnablementService.isEnabled(this.extension.local); - const isRunning = this._runningExtensions.some(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier)); + const isRunning = this.extensionService.extensions.some(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier)); if (isEnabled && isRunning) { if (this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsDependencyChecker.ts b/src/vs/workbench/contrib/extensions/browser/extensionsDependencyChecker.ts index c7c58e35cb8..28b50d2f82b 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsDependencyChecker.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsDependencyChecker.ts @@ -43,10 +43,10 @@ export class ExtensionDependencyChecker extends Disposable implements IWorkbench } private async getAllMissingDependencies(): Promise { - const runningExtensions = await this.extensionService.getExtensions(); - const runningExtensionsIds: Set = runningExtensions.reduce((result, r) => { result.add(r.identifier.value.toLowerCase()); return result; }, new Set()); + await this.extensionService.whenInstalledExtensionsRegistered(); + const runningExtensionsIds: Set = this.extensionService.extensions.reduce((result, r) => { result.add(r.identifier.value.toLowerCase()); return result; }, new Set()); const missingDependencies: Set = new Set(); - for (const extension of runningExtensions) { + for (const extension of this.extensionService.extensions) { if (extension.extensionDependencies) { extension.extensionDependencies.forEach(dep => { if (!runningExtensionsIds.has(dep.toLowerCase())) { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts index c5a4a64a417..931710ee3d5 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts @@ -198,8 +198,7 @@ export class Renderer implements IPagedRenderer { } return !(await this.extensionsWorkbenchService.canInstall(extension)); } else if (extension.local && !isLanguagePackExtension(extension.local.manifest)) { - const runningExtensions = await this.extensionService.getExtensions(); - const runningExtension = runningExtensions.filter(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, extension.identifier))[0]; + const runningExtension = this.extensionService.extensions.filter(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, extension.identifier))[0]; return !(runningExtension && extension.server === this.extensionManagementServerService.getExtensionManagementServer(toExtension(runningExtension))); } return false; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index e94acf71a6a..fb9c4ee55ab 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -329,7 +329,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio */ viewDescriptors.push({ id: 'workbench.views.extensions.searchOutdated', - name: localize('updates', "Updates"), + name: localize('availableUpdates', "Available Updates"), ctorDescriptor: new SyncDescriptor(OutdatedExtensionsView, [{}]), when: ContextKeyExpr.or(SearchExtensionUpdatesContext, ContextKeyExpr.has('searchOutdatedExtensions')), order: 1, @@ -806,9 +806,21 @@ export class StatusUpdater extends Disposable implements IWorkbenchContribution private onServiceChange(): void { this.badgeHandle.clear(); - const outdated = this.extensionsWorkbenchService.outdated.reduce((r, e) => r + (this.extensionEnablementService.isEnabled(e.local!) ? 1 : 0), 0); - if (outdated > 0) { - const badge = new NumberBadge(outdated, n => localize('outdatedExtensions', '{0} Outdated Extensions', n)); + const extensionsReloadRequired = this.extensionsWorkbenchService.installed.filter(e => e.reloadRequiredStatus !== undefined); + const outdated = this.extensionsWorkbenchService.outdated.reduce((r, e) => r + (this.extensionEnablementService.isEnabled(e.local!) && !extensionsReloadRequired.includes(e) ? 1 : 0), 0); + const newBadgeNumber = outdated + extensionsReloadRequired.length; + if (newBadgeNumber > 0) { + let msg = ''; + if (outdated) { + msg += outdated === 1 ? localize('extensionToUpdate', '{0} Extension requires update', outdated) : localize('extensionsToUpdate', '{0} Extensions require update', outdated); + } + if (outdated > 0 && extensionsReloadRequired.length > 0) { + msg += ', '; + } + if (extensionsReloadRequired.length) { + msg += extensionsReloadRequired.length === 1 ? localize('extensionToReload', '{0} Extension requires reload', extensionsReloadRequired.length) : localize('extensionsToReload', '{0} Extensions require reload', extensionsReloadRequired.length); + } + const badge = new NumberBadge(newBadgeNumber, () => msg); this.badgeHandle.value = this.activityService.showViewContainerActivity(VIEWLET_ID, { badge, clazz: 'extensions-badge count-badge' }); } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 86054ea7d42..ec3821e1885 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -296,7 +296,6 @@ export class ExtensionsListView extends ViewPane { private async onContextMenu(e: IListContextMenuEvent): Promise { if (e.element) { - const runningExtensions = await this.extensionService.getExtensions(); const disposables = new DisposableStore(); const manageExtensionAction = disposables.add(this.instantiationService.createInstance(ManageExtensionAction)); const extension = e.element ? this.extensionsWorkbenchService.local.find(local => areSameExtensions(local.identifier, e.element!.identifier) && (!e.element!.server || e.element!.server === local.server)) || e.element @@ -304,7 +303,7 @@ export class ExtensionsListView extends ViewPane { manageExtensionAction.extension = extension; let groups: IAction[][] = []; if (manageExtensionAction.enabled) { - groups = await manageExtensionAction.getActionGroups(runningExtensions); + groups = await manageExtensionAction.getActionGroups(); } else if (extension) { groups = await getContextMenuActions(extension, this.contextKeyService, this.instantiationService); groups.forEach(group => group.forEach(extensionAction => { @@ -375,8 +374,7 @@ export class ExtensionsListView extends ViewPane { private async queryLocal(query: Query, options: IQueryOptions): Promise { const local = await this.extensionsWorkbenchService.queryLocal(this.options.server); - const runningExtensions = await this.extensionService.getExtensions(); - let { extensions, canIncludeInstalledExtensions } = await this.filterLocal(local, runningExtensions, query, options); + let { extensions, canIncludeInstalledExtensions } = await this.filterLocal(local, this.extensionService.extensions, query, options); const disposables = new DisposableStore(); const onDidChangeModel = disposables.add(new Emitter>()); @@ -388,8 +386,7 @@ export class ExtensionsListView extends ViewPane { this.extensionService.onDidChangeExtensions ), () => undefined)(async () => { const local = this.options.server ? this.extensionsWorkbenchService.installed.filter(e => e.server === this.options.server) : this.extensionsWorkbenchService.local; - const runningExtensions = await this.extensionService.getExtensions(); - const { extensions: newExtensions } = await this.filterLocal(local, runningExtensions, query, options); + const { extensions: newExtensions } = await this.filterLocal(local, this.extensionService.extensions, query, options); if (!isDisposed) { const mergedExtensions = this.mergeAddedExtensions(extensions, newExtensions); if (mergedExtensions) { @@ -407,7 +404,7 @@ export class ExtensionsListView extends ViewPane { }; } - private async filterLocal(local: IExtension[], runningExtensions: IExtensionDescription[], query: Query, options: IQueryOptions): Promise<{ extensions: IExtension[]; canIncludeInstalledExtensions: boolean }> { + private async filterLocal(local: IExtension[], runningExtensions: readonly IExtensionDescription[], query: Query, options: IQueryOptions): Promise<{ extensions: IExtension[]; canIncludeInstalledExtensions: boolean }> { const value = query.value; let extensions: IExtension[] = []; let canIncludeInstalledExtensions = true; @@ -510,7 +507,7 @@ export class ExtensionsListView extends ViewPane { return { value, categories }; } - private filterInstalledExtensions(local: IExtension[], runningExtensions: IExtensionDescription[], query: Query, options: IQueryOptions): IExtension[] { + private filterInstalledExtensions(local: IExtension[], runningExtensions: readonly IExtensionDescription[], query: Query, options: IQueryOptions): IExtension[] { let { value, categories } = this.parseCategories(query.value); value = value.replace(/@installed/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase(); @@ -526,7 +523,7 @@ export class ExtensionsListView extends ViewPane { const runningExtensionsById = runningExtensions.reduce((result, e) => { result.set(ExtensionIdentifier.toKey(e.identifier.value), e); return result; }, new Map()); result = result.sort((e1, e2) => { const running1 = runningExtensionsById.get(ExtensionIdentifier.toKey(e1.identifier.id)); - const isE1Running = running1 && this.extensionManagementServerService.getExtensionManagementServer(toExtension(running1)) === e1.server; + const isE1Running = !!running1 && this.extensionManagementServerService.getExtensionManagementServer(toExtension(running1)) === e1.server; const running2 = runningExtensionsById.get(ExtensionIdentifier.toKey(e2.identifier.id)); const isE2Running = running2 && this.extensionManagementServerService.getExtensionManagementServer(toExtension(running2)) === e2.server; if ((isE1Running && isE2Running)) { @@ -566,7 +563,7 @@ export class ExtensionsListView extends ViewPane { return this.sortExtensions(result, options); } - private filterDisabledExtensions(local: IExtension[], runningExtensions: IExtensionDescription[], query: Query, options: IQueryOptions): IExtension[] { + private filterDisabledExtensions(local: IExtension[], runningExtensions: readonly IExtensionDescription[], query: Query, options: IQueryOptions): IExtension[] { let { value, categories } = this.parseCategories(query.value); value = value.replace(/@disabled/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase(); @@ -580,7 +577,7 @@ export class ExtensionsListView extends ViewPane { return this.sortExtensions(result, options); } - private filterEnabledExtensions(local: IExtension[], runningExtensions: IExtensionDescription[], query: Query, options: IQueryOptions): IExtension[] { + private filterEnabledExtensions(local: IExtension[], runningExtensions: readonly IExtensionDescription[], query: Query, options: IQueryOptions): IExtension[] { let { value, categories } = this.parseCategories(query.value); value = value ? value.replace(/@enabled/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase() : ''; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 26f45b2ed19..4dda6675f73 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -34,7 +34,7 @@ import * as resources from 'vs/base/common/resources'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IFileService } from 'vs/platform/files/common/files'; -import { IExtensionManifest, ExtensionType, IExtension as IPlatformExtension, TargetPlatform, ExtensionIdentifier, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { IExtensionManifest, ExtensionType, IExtension as IPlatformExtension, TargetPlatform, ExtensionIdentifier, IExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { IProductService } from 'vs/platform/product/common/productService'; import { FileAccess } from 'vs/base/common/network'; @@ -43,7 +43,7 @@ import { IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDa import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { isBoolean, isUndefined } from 'vs/base/common/types'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; -import { IExtensionService, IExtensionsStatus } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionService, IExtensionsStatus, toExtension, toExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionEditor } from 'vs/workbench/contrib/extensions/browser/extensionEditor'; import { isWeb, language } from 'vs/base/common/platform'; import { ILanguagePackService } from 'vs/platform/languagePacks/common/languagePacks'; @@ -70,6 +70,7 @@ export class Extension implements IExtension { constructor( private stateProvider: IExtensionStateProvider, + private runtimeStateProvider: IExtensionStateProvider, public readonly server: IExtensionManagementServer | undefined, public local: ILocalExtension | undefined, public gallery: IGalleryExtension | undefined, @@ -259,6 +260,10 @@ export class Extension implements IExtension { && semver.eq(this.latestVersion, this.version); } + get reloadRequiredStatus(): string | undefined { + return this.runtimeStateProvider(this); + } + get telemetryData(): any { const { local, gallery } = this; @@ -431,6 +436,7 @@ class Extensions extends Disposable { constructor( readonly server: IExtensionManagementServer, private readonly stateProvider: IExtensionStateProvider, + private readonly runtimeStateProvider: IExtensionStateProvider, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @IInstantiationService private readonly instantiationService: IInstantiationService @@ -466,7 +472,7 @@ class Extensions extends Disposable { const byId = index(this.installed, e => e.local ? e.local.identifier.id : e.identifier.id); this.installed = installed.map(local => { - const extension = byId[local.identifier.id] || this.instantiationService.createInstance(Extension, this.stateProvider, this.server, local, undefined); + const extension = byId[local.identifier.id] || this.instantiationService.createInstance(Extension, this.stateProvider, this.runtimeStateProvider, this.server, local, undefined); extension.local = local; extension.enablementState = this.extensionEnablementService.getEnablementState(local); Extensions.updateExtensionFromControlManifest(extension, extensionsControlManifest); @@ -554,7 +560,7 @@ class Extensions extends Disposable { const { source } = event; if (source && !URI.isUri(source)) { const extension = this.installed.filter(e => areSameExtensions(e.identifier, source.identifier))[0] - || this.instantiationService.createInstance(Extension, this.stateProvider, this.server, undefined, source); + || this.instantiationService.createInstance(Extension, this.stateProvider, this.runtimeStateProvider, this.server, undefined, source); this.installing.push(extension); this._onChange.fire({ extension }); } @@ -564,7 +570,7 @@ class Extensions extends Disposable { const extensionsControlManifest = await this.server.extensionManagementService.getExtensionsControlManifest(); for (const addedExtension of added) { if (this.installed.find(e => areSameExtensions(e.identifier, addedExtension.identifier))) { - const extension = this.instantiationService.createInstance(Extension, this.stateProvider, this.server, addedExtension, undefined); + const extension = this.instantiationService.createInstance(Extension, this.stateProvider, this.runtimeStateProvider, this.server, addedExtension, undefined); this.installed.push(extension); Extensions.updateExtensionFromControlManifest(extension, extensionsControlManifest); } @@ -586,7 +592,7 @@ class Extensions extends Disposable { this.installing = installingExtension ? this.installing.filter(e => e !== installingExtension) : this.installing; let extension: Extension | undefined = installingExtension ? installingExtension - : (location || local) ? this.instantiationService.createInstance(Extension, this.stateProvider, this.server, local, undefined) + : (location || local) ? this.instantiationService.createInstance(Extension, this.stateProvider, this.runtimeStateProvider, this.server, local, undefined) : undefined; if (extension) { if (local) { @@ -722,17 +728,17 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } this.hasOutdatedExtensionsContextKey = HasOutdatedExtensionsContext.bindTo(contextKeyService); if (extensionManagementServerService.localExtensionManagementServer) { - this.localExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.localExtensionManagementServer, ext => this.getExtensionState(ext))); + this.localExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.localExtensionManagementServer, ext => this.getExtensionState(ext), ext => this.getReloadStatus(ext))); this._register(this.localExtensions.onChange(e => this._onChange.fire(e ? e.extension : undefined))); this._register(this.localExtensions.onReset(e => { this._onChange.fire(undefined); this._onReset.fire(); })); } if (extensionManagementServerService.remoteExtensionManagementServer) { - this.remoteExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.remoteExtensionManagementServer, ext => this.getExtensionState(ext))); + this.remoteExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.remoteExtensionManagementServer, ext => this.getExtensionState(ext), ext => this.getReloadStatus(ext))); this._register(this.remoteExtensions.onChange(e => this._onChange.fire(e ? e.extension : undefined))); this._register(this.remoteExtensions.onReset(e => { this._onChange.fire(undefined); this._onReset.fire(); })); } if (extensionManagementServerService.webExtensionManagementServer) { - this.webExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.webExtensionManagementServer, ext => this.getExtensionState(ext))); + this.webExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.webExtensionManagementServer, ext => this.getExtensionState(ext), ext => this.getReloadStatus(ext))); this._register(this.webExtensions.onChange(e => this._onChange.fire(e ? e.extension : undefined))); this._register(this.webExtensions.onReset(e => { this._onChange.fire(undefined); this._onReset.fire(); })); } @@ -767,6 +773,10 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension }, this)); this.queryLocal().then(() => { + this.extensionService.whenInstalledExtensionsRegistered().then(() => { + this.onDidChangeRunningExtensions(this.extensionService.extensions, []); + this._register(this.extensionService.onDidChangeExtensions(({ added, removed }) => this.onDidChangeRunningExtensions(added, removed))); + }); this.resetIgnoreAutoUpdateExtensions(); this.eventuallyCheckForUpdates(true); this._reportTelemetry(); @@ -792,6 +802,22 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension this.telemetryService.publicLog2('installedExtensions', { extensionIds: extensionIds.join(';'), count: extensionIds.length }); } + private async onDidChangeRunningExtensions(added: ReadonlyArray, removed: ReadonlyArray): Promise { + const local = this.local; + const changedExtensions: IExtension[] = []; + const extsNotInstalled: IExtensionInfo[] = []; + for (const desc of added) { + const extension = local.find(e => areSameExtensions({ id: desc.identifier.value, uuid: desc.uuid }, e.identifier)); + if (extension) { + changedExtensions.push(extension); + } else { + extsNotInstalled.push({ id: desc.identifier.value, uuid: desc.uuid }); + } + } + changedExtensions.push(...await this.getExtensions(extsNotInstalled, CancellationToken.None)); + changedExtensions.forEach(e => this._onChange.fire(e)); + } + get local(): IExtension[] { const byId = groupByExtension(this.installed, r => r.identifier); return byId.reduce((result, extensions) => { result.push(this.getPrimaryExtension(extensions)); return result; }, []); @@ -932,7 +958,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private fromGallery(gallery: IGalleryExtension, extensionsControlManifest: IExtensionsControlManifest): IExtension { let extension = this.getInstalledExtensionMatchingGallery(gallery); if (!extension) { - extension = this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), undefined, undefined, gallery); + extension = this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), ext => this.getReloadStatus(ext), undefined, undefined, gallery); Extensions.updateExtensionFromControlManifest(extension, extensionsControlManifest); } return extension; @@ -970,6 +996,93 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return undefined; } + private getReloadStatus(extension: IExtension): string | undefined { + const isUninstalled = extension.state === ExtensionState.Uninstalled; + const runningExtension = this.extensionService.extensions.find(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, extension!.identifier)); + + if (isUninstalled) { + const canRemoveRunningExtension = runningExtension && this.extensionService.canRemoveExtension(runningExtension); + const isSameExtensionRunning = runningExtension && (!extension.server || extension.server === this.extensionManagementServerService.getExtensionManagementServer(toExtension(runningExtension))); + if (!canRemoveRunningExtension && isSameExtensionRunning) { + return nls.localize('postUninstallTooltip', "Please reload Visual Studio Code to complete the uninstallation of this extension."); + } + return undefined; + } + if (extension.local) { + const isSameExtensionRunning = runningExtension && extension.server === this.extensionManagementServerService.getExtensionManagementServer(toExtension(runningExtension)); + const isEnabled = this.extensionEnablementService.isEnabled(extension.local); + + // Extension is running + if (runningExtension) { + if (isEnabled) { + // No Reload is required if extension can run without reload + if (this.extensionService.canAddExtension(toExtensionDescription(extension.local))) { + return undefined; + } + const runningExtensionServer = this.extensionManagementServerService.getExtensionManagementServer(toExtension(runningExtension)); + + if (isSameExtensionRunning) { + // Different version or target platform of same extension is running. Requires reload to run the current version + if (extension.version !== runningExtension.version || extension.local.targetPlatform !== runningExtension.targetPlatform) { + return nls.localize('postUpdateTooltip', "Please reload Visual Studio Code to enable the updated extension."); + } + + const extensionInOtherServer = this.installed.filter(e => areSameExtensions(e.identifier, extension!.identifier) && e.server !== extension!.server)[0]; + if (extensionInOtherServer) { + // This extension prefers to run on UI/Local side but is running in remote + if (runningExtensionServer === this.extensionManagementServerService.remoteExtensionManagementServer && this.extensionManifestPropertiesService.prefersExecuteOnUI(extension.local!.manifest)) { + return nls.localize('enable locally', "Please reload Visual Studio Code to enable this extension locally."); + } + + // This extension prefers to run on Workspace/Remote side but is running in local + if (runningExtensionServer === this.extensionManagementServerService.localExtensionManagementServer && this.extensionManifestPropertiesService.prefersExecuteOnWorkspace(extension.local!.manifest)) { + return nls.localize('enable remote', "Please reload Visual Studio Code to enable this extension in {0}.", this.extensionManagementServerService.remoteExtensionManagementServer?.label); + } + } + + } else { + + if (extension.server === this.extensionManagementServerService.localExtensionManagementServer && runningExtensionServer === this.extensionManagementServerService.remoteExtensionManagementServer) { + // This extension prefers to run on UI/Local side but is running in remote + if (this.extensionManifestPropertiesService.prefersExecuteOnUI(extension.local!.manifest)) { + return nls.localize('postEnableTooltip', "Please reload Visual Studio Code to enable this extension."); + } + } + if (extension.server === this.extensionManagementServerService.remoteExtensionManagementServer && runningExtensionServer === this.extensionManagementServerService.localExtensionManagementServer) { + // This extension prefers to run on Workspace/Remote side but is running in local + if (this.extensionManifestPropertiesService.prefersExecuteOnWorkspace(extension.local!.manifest)) { + return nls.localize('postEnableTooltip', "Please reload Visual Studio Code to enable this extension."); + } + } + } + return undefined; + } else { + if (isSameExtensionRunning) { + return nls.localize('postDisableTooltip', "Please reload Visual Studio Code to disable this extension."); + } + } + return undefined; + } + + // Extension is not running + else { + if (isEnabled && !this.extensionService.canAddExtension(toExtensionDescription(extension.local))) { + return nls.localize('postEnableTooltip', "Please reload Visual Studio Code to enable this extension."); + } + + const otherServer = extension.server ? extension.server === this.extensionManagementServerService.localExtensionManagementServer ? this.extensionManagementServerService.remoteExtensionManagementServer : this.extensionManagementServerService.localExtensionManagementServer : null; + if (otherServer && extension.enablementState === EnablementState.DisabledByExtensionKind) { + const extensionInOtherServer = this.local.filter(e => areSameExtensions(e.identifier, extension!.identifier) && e.server === otherServer)[0]; + // Same extension in other server exists and + if (extensionInOtherServer && extensionInOtherServer.local && this.extensionEnablementService.isEnabled(extensionInOtherServer.local)) { + return nls.localize('postEnableTooltip', "Please reload Visual Studio Code to enable this extension."); + } + } + } + } + return undefined; + } + private getPrimaryExtension(extensions: IExtension[]): IExtension { if (extensions.length === 1) { return extensions[0]; diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index 32c2becd92f..9b6ca4e0345 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -65,6 +65,7 @@ export interface IExtension { readonly ratingCount?: number; readonly outdated: boolean; readonly outdatedTargetPlatform: boolean; + readonly reloadRequiredStatus?: string; readonly enablementState: EnablementState; readonly tags: readonly string[]; readonly categories: readonly string[]; diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extension.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extension.test.ts index f07341fe991..ca40188231b 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extension.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extension.test.ts @@ -24,78 +24,78 @@ suite('Extension Test', () => { }); test('extension is not outdated when there is no local and gallery', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, undefined, undefined); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, undefined, undefined); assert.strictEqual(extension.outdated, false); }); test('extension is not outdated when there is local and no gallery', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, aLocalExtension(), undefined); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension(), undefined); assert.strictEqual(extension.outdated, false); }); test('extension is not outdated when there is no local and has gallery', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, undefined, aGalleryExtension()); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, undefined, aGalleryExtension()); assert.strictEqual(extension.outdated, false); }); test('extension is not outdated when local and gallery are on same version', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, aLocalExtension(), aGalleryExtension()); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension(), aGalleryExtension()); assert.strictEqual(extension.outdated, false); }); test('extension is outdated when local is older than gallery', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, aLocalExtension('somext', { version: '1.0.0' }), aGalleryExtension('somext', { version: '1.0.1' })); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }), aGalleryExtension('somext', { version: '1.0.1' })); assert.strictEqual(extension.outdated, true); }); test('extension is outdated when local is built in and older than gallery', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, aLocalExtension('somext', { version: '1.0.0' }, { type: ExtensionType.System }), aGalleryExtension('somext', { version: '1.0.1' })); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }, { type: ExtensionType.System }), aGalleryExtension('somext', { version: '1.0.1' })); assert.strictEqual(extension.outdated, true); }); test('extension is not outdated when local is built in and older than gallery but product quality is stable', () => { instantiationService.stub(IProductService, >{ quality: 'stable' }); - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, aLocalExtension('somext', { version: '1.0.0' }, { type: ExtensionType.System }), aGalleryExtension('somext', { version: '1.0.1' })); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }, { type: ExtensionType.System }), aGalleryExtension('somext', { version: '1.0.1' })); assert.strictEqual(extension.outdated, false); }); test('extension is outdated when local and gallery are on same version but on different target platforms', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, aLocalExtension('somext', {}, { targetPlatform: TargetPlatform.WIN32_IA32 }), aGalleryExtension('somext', {}, { targetPlatform: TargetPlatform.WIN32_X64 })); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', {}, { targetPlatform: TargetPlatform.WIN32_IA32 }), aGalleryExtension('somext', {}, { targetPlatform: TargetPlatform.WIN32_X64 })); assert.strictEqual(extension.outdated, true); }); test('extension is not outdated when local and gallery are on same version and local is on web', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, aLocalExtension('somext', {}, { targetPlatform: TargetPlatform.WEB }), aGalleryExtension('somext')); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', {}, { targetPlatform: TargetPlatform.WEB }), aGalleryExtension('somext')); assert.strictEqual(extension.outdated, false); }); test('extension is not outdated when local and gallery are on same version and gallery is on web', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, aLocalExtension('somext'), aGalleryExtension('somext', {}, { targetPlatform: TargetPlatform.WEB })); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext'), aGalleryExtension('somext', {}, { targetPlatform: TargetPlatform.WEB })); assert.strictEqual(extension.outdated, false); }); test('extension is not outdated when local is not pre-release but gallery is pre-release', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, aLocalExtension('somext', { version: '1.0.0' }), aGalleryExtension('somext', { version: '1.0.1' }, { isPreReleaseVersion: true })); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }), aGalleryExtension('somext', { version: '1.0.1' }, { isPreReleaseVersion: true })); assert.strictEqual(extension.outdated, false); }); test('extension is outdated when local and gallery are pre-releases', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, aLocalExtension('somext', { version: '1.0.0' }, { preRelease: true, isPreReleaseVersion: true }), aGalleryExtension('somext', { version: '1.0.1' }, { isPreReleaseVersion: true })); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }, { preRelease: true, isPreReleaseVersion: true }), aGalleryExtension('somext', { version: '1.0.1' }, { isPreReleaseVersion: true })); assert.strictEqual(extension.outdated, true); }); test('extension is outdated when local was opted to pre-release but current version is not pre-release', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, aLocalExtension('somext', { version: '1.0.0' }, { preRelease: true, isPreReleaseVersion: false }), aGalleryExtension('somext', { version: '1.0.1' }, { isPreReleaseVersion: true })); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }, { preRelease: true, isPreReleaseVersion: false }), aGalleryExtension('somext', { version: '1.0.1' }, { isPreReleaseVersion: true })); assert.strictEqual(extension.outdated, true); }); test('extension is outdated when local is pre-release but gallery is not', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, aLocalExtension('somext', { version: '1.0.0' }, { preRelease: true, isPreReleaseVersion: true }), aGalleryExtension('somext', { version: '1.0.1' })); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }, { preRelease: true, isPreReleaseVersion: true }), aGalleryExtension('somext', { version: '1.0.1' })); assert.strictEqual(extension.outdated, true); }); test('extension is outdated when local was opted pre-release but current version is not and gallery is not', () => { - const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, aLocalExtension('somext', { version: '1.0.0' }, { preRelease: true, isPreReleaseVersion: false }), aGalleryExtension('somext', { version: '1.0.1' })); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, () => undefined, undefined, aLocalExtension('somext', { version: '1.0.0' }, { preRelease: true, isPreReleaseVersion: false }), aGalleryExtension('somext', { version: '1.0.1' })); assert.strictEqual(extension.outdated, true); }); diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts index e7f9aabc4ce..b9801ecbecb 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts @@ -223,6 +223,8 @@ suite('ExtensionRecommendationsService Test', () => { async getTargetPlatform() { return getTargetPlatform(platform, arch); } }); instantiationService.stub(IExtensionService, >{ + onDidChangeExtensions: Event.None, + extensions: [], async whenInstalledExtensionsRegistered() { return true; } }); instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts index 0d52cc8679a..84f36953148 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts @@ -34,7 +34,7 @@ import { URI } from 'vs/base/common/uri'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { RemoteAgentService } from 'vs/workbench/services/remote/electron-sandbox/remoteAgentService'; -import { ExtensionIdentifier, IExtensionContributions, ExtensionType, IExtensionDescription, IExtension } from 'vs/platform/extensions/common/extensions'; +import { IExtensionContributions, ExtensionType, IExtensionDescription, IExtension } from 'vs/platform/extensions/common/extensions'; import { ISharedProcessService } from 'vs/platform/ipc/electron-sandbox/services'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ILabelService, IFormatterChangeEvent } from 'vs/platform/label/common/label'; @@ -138,7 +138,7 @@ async function setupTest() { instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage()); instantiationService.stubPromise(IExtensionGalleryService, 'getExtensions', []); - instantiationService.stub(IExtensionService, >{ getExtensions: () => Promise.resolve([]), onDidChangeExtensions: new Emitter().event, canAddExtension: (extension: IExtensionDescription) => false, canRemoveExtension: (extension: IExtensionDescription) => false }); + instantiationService.stub(IExtensionService, >{ extensions: [], onDidChangeExtensions: Event.None, canAddExtension: (extension: IExtensionDescription) => false, canRemoveExtension: (extension: IExtensionDescription) => false }); (instantiationService.get(IWorkbenchExtensionEnablementService)).reset(); instantiationService.stub(IUserDataSyncEnablementService, instantiationService.createInstance(UserDataSyncEnablementService)); @@ -743,7 +743,7 @@ suite('ExtensionsActions', () => { }); test('Test DisableForWorkspaceAction when there is no extension', () => { - const testObject: ExtensionsActions.DisableForWorkspaceAction = instantiationService.createInstance(ExtensionsActions.DisableForWorkspaceAction, []); + const testObject: ExtensionsActions.DisableForWorkspaceAction = instantiationService.createInstance(ExtensionsActions.DisableForWorkspaceAction); assert.ok(!testObject.enabled); }); @@ -756,7 +756,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.DisableForWorkspaceAction = instantiationService.createInstance(ExtensionsActions.DisableForWorkspaceAction, []); + const testObject: ExtensionsActions.DisableForWorkspaceAction = instantiationService.createInstance(ExtensionsActions.DisableForWorkspaceAction); testObject.extension = extensions[0]; assert.ok(!testObject.enabled); }); @@ -771,7 +771,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.DisableForWorkspaceAction = instantiationService.createInstance(ExtensionsActions.DisableForWorkspaceAction, []); + const testObject: ExtensionsActions.DisableForWorkspaceAction = instantiationService.createInstance(ExtensionsActions.DisableForWorkspaceAction); testObject.extension = extensions[0]; assert.ok(!testObject.enabled); }); @@ -780,18 +780,23 @@ suite('ExtensionsActions', () => { test('Test DisableForWorkspaceAction when extension is enabled', () => { const local = aLocalExtension('a'); + instantiationService.stub(IExtensionService, >{ + extensions: [toExtensionDescription(local)], + onDidChangeExtensions: Event.None, + }); + instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.DisableForWorkspaceAction = instantiationService.createInstance(ExtensionsActions.DisableForWorkspaceAction, [{ identifier: new ExtensionIdentifier('pub.a'), extensionLocation: URI.file('pub.a') }]); + const testObject: ExtensionsActions.DisableForWorkspaceAction = instantiationService.createInstance(ExtensionsActions.DisableForWorkspaceAction); testObject.extension = extensions[0]; assert.ok(testObject.enabled); }); }); test('Test DisableGloballyAction when there is no extension', () => { - const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction, []); + const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction); assert.ok(!testObject.enabled); }); @@ -804,7 +809,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction, []); + const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction); testObject.extension = extensions[0]; assert.ok(!testObject.enabled); }); @@ -819,7 +824,7 @@ suite('ExtensionsActions', () => { return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction, []); + const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction); testObject.extension = extensions[0]; assert.ok(!testObject.enabled); }); @@ -829,10 +834,14 @@ suite('ExtensionsActions', () => { test('Test DisableGloballyAction when the extension is enabled', () => { const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); + instantiationService.stub(IExtensionService, >{ + extensions: [toExtensionDescription(local)], + onDidChangeExtensions: Event.None, + }); return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction, [{ identifier: new ExtensionIdentifier('pub.a'), extensionLocation: URI.file('pub.a') }]); + const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction); testObject.extension = extensions[0]; assert.ok(testObject.enabled); }); @@ -841,10 +850,14 @@ suite('ExtensionsActions', () => { test('Test DisableGloballyAction when extension is installed and enabled', () => { const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); + instantiationService.stub(IExtensionService, >{ + extensions: [toExtensionDescription(local)], + onDidChangeExtensions: Event.None, + }); return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction, [{ identifier: new ExtensionIdentifier('pub.a'), extensionLocation: URI.file('pub.a') }]); + const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction); testObject.extension = extensions[0]; assert.ok(testObject.enabled); }); @@ -852,13 +865,18 @@ suite('ExtensionsActions', () => { test('Test DisableGloballyAction when extension is installed and disabled globally', () => { const local = aLocalExtension('a'); + instantiationService.stub(IExtensionService, >{ + extensions: [toExtensionDescription(local)], + onDidChangeExtensions: Event.None, + }); + return instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([local], EnablementState.DisabledGlobally) .then(() => { instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction, [{ identifier: new ExtensionIdentifier('pub.a'), extensionLocation: URI.file('pub.a') }]); + const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction); testObject.extension = extensions[0]; assert.ok(!testObject.enabled); }); @@ -868,10 +886,14 @@ suite('ExtensionsActions', () => { test('Test DisableGloballyAction when extension is uninstalled', () => { const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); + instantiationService.stub(IExtensionService, >{ + extensions: [toExtensionDescription(aLocalExtension('a'))], + onDidChangeExtensions: Event.None, + }); return instantiationService.get(IExtensionsWorkbenchService).queryGallery(CancellationToken.None) .then(page => { - const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction, [{ identifier: new ExtensionIdentifier('pub.a'), extensionLocation: URI.file('pub.a') }]); + const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction); testObject.extension = page.firstPage[0]; assert.ok(!testObject.enabled); }); @@ -880,10 +902,14 @@ suite('ExtensionsActions', () => { test('Test DisableGloballyAction when extension is installing', () => { const gallery = aGalleryExtension('a'); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); + instantiationService.stub(IExtensionService, >{ + extensions: [toExtensionDescription(aLocalExtension('a'))], + onDidChangeExtensions: Event.None, + }); return instantiationService.get(IExtensionsWorkbenchService).queryGallery(CancellationToken.None) .then(page => { - const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction, [{ identifier: new ExtensionIdentifier('pub.a'), extensionLocation: URI.file('pub.a') }]); + const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction); testObject.extension = page.firstPage[0]; instantiationService.createInstance(ExtensionContainers, [testObject]); installEvent.fire({ identifier: gallery.identifier, source: gallery }); @@ -894,10 +920,14 @@ suite('ExtensionsActions', () => { test('Test DisableGloballyAction when extension is uninstalling', () => { const local = aLocalExtension('a'); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); + instantiationService.stub(IExtensionService, >{ + extensions: [toExtensionDescription(local)], + onDidChangeExtensions: Event.None, + }); return instantiationService.get(IExtensionsWorkbenchService).queryLocal() .then(extensions => { - const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction, [{ identifier: new ExtensionIdentifier('pub.a'), extensionLocation: URI.file('pub.a') }]); + const testObject: ExtensionsActions.DisableGloballyAction = instantiationService.createInstance(ExtensionsActions.DisableGloballyAction); testObject.extension = extensions[0]; instantiationService.createInstance(ExtensionContainers, [testObject]); uninstallEvent.fire({ identifier: local.identifier }); @@ -945,10 +975,9 @@ suite('ReloadAction', () => { }); test('Test ReloadAction when extension is newly installed', async () => { - const onDidChangeExtensionsEmitter: Emitter = new Emitter(); - const runningExtensions = [toExtensionDescription(aLocalExtension('b'))]; + const onDidChangeExtensionsEmitter = new Emitter<{ added: IExtensionDescription[]; removed: IExtensionDescription[] }>(); instantiationService.stub(IExtensionService, >{ - getExtensions: () => Promise.resolve(runningExtensions), + extensions: [toExtensionDescription(aLocalExtension('b'))], onDidChangeExtensions: onDidChangeExtensionsEmitter.event, canAddExtension: (extension) => false }); @@ -970,10 +999,9 @@ suite('ReloadAction', () => { }); test('Test ReloadAction when extension is newly installed and reload is not required', async () => { - const onDidChangeExtensionsEmitter: Emitter = new Emitter(); - const runningExtensions = [toExtensionDescription(aLocalExtension('b'))]; + const onDidChangeExtensionsEmitter = new Emitter<{ added: IExtensionDescription[]; removed: IExtensionDescription[] }>(); instantiationService.stub(IExtensionService, >{ - getExtensions: () => Promise.resolve(runningExtensions), + extensions: [toExtensionDescription(aLocalExtension('b'))], onDidChangeExtensions: onDidChangeExtensionsEmitter.event, canAddExtension: (extension) => true }); @@ -992,7 +1020,12 @@ suite('ReloadAction', () => { }); test('Test ReloadAction when extension is installed and uninstalled', async () => { - instantiationService.stubPromise(IExtensionService, 'getExtensions', [toExtensionDescription(aLocalExtension('b'))]); + instantiationService.stub(IExtensionService, >{ + extensions: [toExtensionDescription(aLocalExtension('b'))], + onDidChangeExtensions: Event.None, + canRemoveExtension: (extension) => false, + canAddExtension: (extension) => false + }); const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); instantiationService.createInstance(ExtensionContainers, [testObject]); const gallery = aGalleryExtension('a'); @@ -1010,7 +1043,13 @@ suite('ReloadAction', () => { }); test('Test ReloadAction when extension is uninstalled', async () => { - instantiationService.stubPromise(IExtensionService, 'getExtensions', [toExtensionDescription(aLocalExtension('a', { version: '1.0.0' }))]); + instantiationService.stub(IExtensionService, >{ + extensions: [toExtensionDescription(aLocalExtension('a', { version: '1.0.0' }))], + onDidChangeExtensions: Event.None, + canRemoveExtension: (extension) => false, + canAddExtension: (extension) => false + }); + instantiationService.set(IExtensionsWorkbenchService, instantiationService.createInstance(ExtensionsWorkbenchService)); const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); instantiationService.createInstance(ExtensionContainers, [testObject]); const local = aLocalExtension('a'); @@ -1027,8 +1066,8 @@ suite('ReloadAction', () => { test('Test ReloadAction when extension is uninstalled and can be removed', async () => { const local = aLocalExtension('a'); instantiationService.stub(IExtensionService, >{ - getExtensions: () => Promise.resolve([toExtensionDescription(local)]), - onDidChangeExtensions: new Emitter().event, + extensions: [toExtensionDescription(local)], + onDidChangeExtensions: Event.None, canRemoveExtension: (extension) => true, canAddExtension: (extension) => true }); @@ -1044,7 +1083,12 @@ suite('ReloadAction', () => { }); test('Test ReloadAction when extension is uninstalled and installed', async () => { - instantiationService.stubPromise(IExtensionService, 'getExtensions', [toExtensionDescription(aLocalExtension('a', { version: '1.0.0' }))]); + instantiationService.stub(IExtensionService, >{ + extensions: [toExtensionDescription(aLocalExtension('a', { version: '1.0.0' }))], + onDidChangeExtensions: Event.None, + canRemoveExtension: (extension) => false, + canAddExtension: (extension) => false + }); const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); instantiationService.createInstance(ExtensionContainers, [testObject]); const local = aLocalExtension('a'); @@ -1064,7 +1108,13 @@ suite('ReloadAction', () => { }); test('Test ReloadAction when extension is updated while running', async () => { - instantiationService.stubPromise(IExtensionService, 'getExtensions', [toExtensionDescription(aLocalExtension('a', { version: '1.0.1' }))]); + instantiationService.stub(IExtensionService, >{ + extensions: [toExtensionDescription(aLocalExtension('a', { version: '1.0.1' }))], + onDidChangeExtensions: Event.None, + canRemoveExtension: (extension) => true, + canAddExtension: (extension) => false + }); + instantiationService.set(IExtensionsWorkbenchService, instantiationService.createInstance(ExtensionsWorkbenchService)); const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); instantiationService.createInstance(ExtensionContainers, [testObject]); const local = aLocalExtension('a', { version: '1.0.1' }); @@ -1086,7 +1136,12 @@ suite('ReloadAction', () => { }); test('Test ReloadAction when extension is updated when not running', async () => { - instantiationService.stubPromise(IExtensionService, 'getExtensions', [toExtensionDescription(aLocalExtension('b'))]); + instantiationService.stub(IExtensionService, >{ + extensions: [toExtensionDescription(aLocalExtension('b'))], + onDidChangeExtensions: Event.None, + canRemoveExtension: (extension) => false, + canAddExtension: (extension) => false + }); const local = aLocalExtension('a', { version: '1.0.1' }); await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([local], EnablementState.DisabledGlobally); const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); @@ -1104,7 +1159,13 @@ suite('ReloadAction', () => { }); test('Test ReloadAction when extension is disabled when running', async () => { - instantiationService.stubPromise(IExtensionService, 'getExtensions', [toExtensionDescription(aLocalExtension('a'))]); + instantiationService.stub(IExtensionService, >{ + extensions: [toExtensionDescription(aLocalExtension('a'))], + onDidChangeExtensions: Event.None, + canRemoveExtension: (extension) => false, + canAddExtension: (extension) => false + }); + instantiationService.set(IExtensionsWorkbenchService, instantiationService.createInstance(ExtensionsWorkbenchService)); const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); instantiationService.createInstance(ExtensionContainers, [testObject]); const local = aLocalExtension('a'); @@ -1120,7 +1181,13 @@ suite('ReloadAction', () => { }); test('Test ReloadAction when extension enablement is toggled when running', async () => { - instantiationService.stubPromise(IExtensionService, 'getExtensions', [toExtensionDescription(aLocalExtension('a', { version: '1.0.0' }))]); + instantiationService.stub(IExtensionService, >{ + extensions: [toExtensionDescription(aLocalExtension('a', { version: '1.0.0' }))], + onDidChangeExtensions: Event.None, + canRemoveExtension: (extension) => false, + canAddExtension: (extension) => false + }); + instantiationService.set(IExtensionsWorkbenchService, instantiationService.createInstance(ExtensionsWorkbenchService)); const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); instantiationService.createInstance(ExtensionContainers, [testObject]); const local = aLocalExtension('a'); @@ -1134,7 +1201,12 @@ suite('ReloadAction', () => { }); test('Test ReloadAction when extension is enabled when not running', async () => { - instantiationService.stubPromise(IExtensionService, 'getExtensions', [toExtensionDescription(aLocalExtension('b'))]); + instantiationService.stub(IExtensionService, >{ + extensions: [toExtensionDescription(aLocalExtension('b'))], + onDidChangeExtensions: Event.None, + canRemoveExtension: (extension) => false, + canAddExtension: (extension) => false + }); const local = aLocalExtension('a'); await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([local], EnablementState.DisabledGlobally); const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); @@ -1150,7 +1222,12 @@ suite('ReloadAction', () => { }); test('Test ReloadAction when extension enablement is toggled when not running', async () => { - instantiationService.stubPromise(IExtensionService, 'getExtensions', [toExtensionDescription(aLocalExtension('b'))]); + instantiationService.stub(IExtensionService, >{ + extensions: [toExtensionDescription(aLocalExtension('b'))], + onDidChangeExtensions: Event.None, + canRemoveExtension: (extension) => false, + canAddExtension: (extension) => false + }); const local = aLocalExtension('a'); await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([local], EnablementState.DisabledGlobally); const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); @@ -1165,7 +1242,12 @@ suite('ReloadAction', () => { }); test('Test ReloadAction when extension is updated when not running and enabled', async () => { - instantiationService.stubPromise(IExtensionService, 'getExtensions', [toExtensionDescription(aLocalExtension('b'))]); + instantiationService.stub(IExtensionService, >{ + extensions: [toExtensionDescription(aLocalExtension('a'))], + onDidChangeExtensions: Event.None, + canRemoveExtension: (extension) => false, + canAddExtension: (extension) => false + }); const local = aLocalExtension('a', { version: '1.0.1' }); await instantiationService.get(IWorkbenchExtensionEnablementService).setEnablement([local], EnablementState.DisabledGlobally); const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); @@ -1185,7 +1267,12 @@ suite('ReloadAction', () => { }); test('Test ReloadAction when a localization extension is newly installed', async () => { - instantiationService.stubPromise(IExtensionService, 'getExtensions', [toExtensionDescription(aLocalExtension('b'))]); + instantiationService.stub(IExtensionService, >{ + extensions: [toExtensionDescription(aLocalExtension('b'))], + onDidChangeExtensions: Event.None, + canRemoveExtension: (extension) => false, + canAddExtension: (extension) => false + }); const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); instantiationService.createInstance(ExtensionContainers, [testObject]); const gallery = aGalleryExtension('a'); @@ -1201,7 +1288,12 @@ suite('ReloadAction', () => { }); test('Test ReloadAction when a localization extension is updated while running', async () => { - instantiationService.stubPromise(IExtensionService, 'getExtensions', [toExtensionDescription(aLocalExtension('a', { version: '1.0.1' }))]); + instantiationService.stub(IExtensionService, >{ + extensions: [toExtensionDescription(aLocalExtension('a', { version: '1.0.1' }))], + onDidChangeExtensions: Event.None, + canRemoveExtension: (extension) => false, + canAddExtension: (extension) => false + }); const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); instantiationService.createInstance(ExtensionContainers, [testObject]); const local = aLocalExtension('a', { version: '1.0.1', contributes: { localizations: [{ languageId: 'de', translations: [] }] } }); @@ -1224,16 +1316,15 @@ suite('ReloadAction', () => { const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, createExtensionManagementService([localExtension]), createExtensionManagementService([remoteExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); - instantiationService.set(IExtensionsWorkbenchService, workbenchService); - - const onDidChangeExtensionsEmitter: Emitter = new Emitter(); - const runningExtensions = [toExtensionDescription(remoteExtension)]; + const onDidChangeExtensionsEmitter = new Emitter<{ added: IExtensionDescription[]; removed: IExtensionDescription[] }>(); instantiationService.stub(IExtensionService, >{ - getExtensions: () => Promise.resolve(runningExtensions), + extensions: [toExtensionDescription(remoteExtension)], onDidChangeExtensions: onDidChangeExtensionsEmitter.event, canAddExtension: (extension) => false }); + const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.set(IExtensionsWorkbenchService, workbenchService); + const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); instantiationService.createInstance(ExtensionContainers, [testObject]); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1258,16 +1349,15 @@ suite('ReloadAction', () => { const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, localExtensionManagementService, createExtensionManagementService([remoteExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); - instantiationService.set(IExtensionsWorkbenchService, workbenchService); - - const onDidChangeExtensionsEmitter: Emitter = new Emitter(); - const runningExtensions = [toExtensionDescription(remoteExtension)]; + const onDidChangeExtensionsEmitter = new Emitter<{ added: IExtensionDescription[]; removed: IExtensionDescription[] }>(); instantiationService.stub(IExtensionService, >{ - getExtensions: () => Promise.resolve(runningExtensions), + extensions: [toExtensionDescription(remoteExtension)], onDidChangeExtensions: onDidChangeExtensionsEmitter.event, canAddExtension: (extension) => false }); + const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.set(IExtensionsWorkbenchService, workbenchService); + const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); instantiationService.createInstance(ExtensionContainers, [testObject]); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1297,9 +1387,9 @@ suite('ReloadAction', () => { const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); instantiationService.set(IExtensionsWorkbenchService, workbenchService); - const onDidChangeExtensionsEmitter: Emitter = new Emitter(); + const onDidChangeExtensionsEmitter = new Emitter<{ added: IExtensionDescription[]; removed: IExtensionDescription[] }>(); instantiationService.stub(IExtensionService, >{ - getExtensions: () => Promise.resolve([]), + extensions: [], onDidChangeExtensions: onDidChangeExtensionsEmitter.event, canAddExtension: (extension) => false }); @@ -1335,9 +1425,9 @@ suite('ReloadAction', () => { const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); instantiationService.set(IExtensionsWorkbenchService, workbenchService); - const onDidChangeExtensionsEmitter: Emitter = new Emitter(); + const onDidChangeExtensionsEmitter = new Emitter<{ added: IExtensionDescription[]; removed: IExtensionDescription[] }>(); instantiationService.stub(IExtensionService, >{ - getExtensions: () => Promise.resolve([]), + extensions: [], onDidChangeExtensions: onDidChangeExtensionsEmitter.event, canAddExtension: (extension) => false }); @@ -1371,15 +1461,15 @@ suite('ReloadAction', () => { const extensionManagementServerService = aMultiExtensionManagementServerService(instantiationService, localExtensionManagementService, createExtensionManagementService([remoteExtension])); instantiationService.stub(IExtensionManagementServerService, extensionManagementServerService); instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); - instantiationService.set(IExtensionsWorkbenchService, workbenchService); - - const onDidChangeExtensionsEmitter: Emitter = new Emitter(); + const onDidChangeExtensionsEmitter = new Emitter<{ added: IExtensionDescription[]; removed: IExtensionDescription[] }>(); instantiationService.stub(IExtensionService, >{ - getExtensions: () => Promise.resolve([toExtensionDescription(localExtension)]), + extensions: [toExtensionDescription(localExtension)], onDidChangeExtensions: onDidChangeExtensionsEmitter.event, canAddExtension: (extension) => false }); + const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); + instantiationService.set(IExtensionsWorkbenchService, workbenchService); + const testObject: ExtensionsActions.ReloadAction = instantiationService.createInstance(ExtensionsActions.ReloadAction); instantiationService.createInstance(ExtensionContainers, [testObject]); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery)); @@ -1405,9 +1495,9 @@ suite('ReloadAction', () => { const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); instantiationService.set(IExtensionsWorkbenchService, workbenchService); - const onDidChangeExtensionsEmitter: Emitter = new Emitter(); + const onDidChangeExtensionsEmitter = new Emitter<{ added: IExtensionDescription[]; removed: IExtensionDescription[] }>(); instantiationService.stub(IExtensionService, >{ - getExtensions: () => Promise.resolve([toExtensionDescription(localExtension)]), + extensions: [toExtensionDescription(localExtension)], onDidChangeExtensions: onDidChangeExtensionsEmitter.event, canAddExtension: (extension) => false }); @@ -1436,9 +1526,9 @@ suite('ReloadAction', () => { const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); instantiationService.set(IExtensionsWorkbenchService, workbenchService); - const onDidChangeExtensionsEmitter: Emitter = new Emitter(); + const onDidChangeExtensionsEmitter = new Emitter<{ added: IExtensionDescription[]; removed: IExtensionDescription[] }>(); instantiationService.stub(IExtensionService, >{ - getExtensions: () => Promise.resolve([toExtensionDescription(remoteExtension)]), + extensions: [toExtensionDescription(remoteExtension)], onDidChangeExtensions: onDidChangeExtensionsEmitter.event, canAddExtension: (extension) => false }); @@ -1467,9 +1557,9 @@ suite('ReloadAction', () => { const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); instantiationService.set(IExtensionsWorkbenchService, workbenchService); - const onDidChangeExtensionsEmitter: Emitter = new Emitter(); + const onDidChangeExtensionsEmitter = new Emitter<{ added: IExtensionDescription[]; removed: IExtensionDescription[] }>(); instantiationService.stub(IExtensionService, >{ - getExtensions: () => Promise.resolve([toExtensionDescription(localExtension)]), + extensions: [toExtensionDescription(localExtension)], onDidChangeExtensions: onDidChangeExtensionsEmitter.event, canAddExtension: (extension) => false }); @@ -1498,9 +1588,9 @@ suite('ReloadAction', () => { const workbenchService: IExtensionsWorkbenchService = instantiationService.createInstance(ExtensionsWorkbenchService); instantiationService.set(IExtensionsWorkbenchService, workbenchService); - const onDidChangeExtensionsEmitter: Emitter = new Emitter(); + const onDidChangeExtensionsEmitter = new Emitter<{ added: IExtensionDescription[]; removed: IExtensionDescription[] }>(); instantiationService.stub(IExtensionService, >{ - getExtensions: () => Promise.resolve([toExtensionDescription(remoteExtension)]), + extensions: [toExtensionDescription(remoteExtension)], onDidChangeExtensions: onDidChangeExtensionsEmitter.event, canAddExtension: (extension) => false }); diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts index 8fdc1786c8f..d513feae320 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts @@ -36,7 +36,7 @@ import { SinonStub } from 'sinon'; import { IExperimentService, ExperimentState, ExperimentActionType, ExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { RemoteAgentService } from 'vs/workbench/services/remote/electron-sandbox/remoteAgentService'; -import { ExtensionType, IExtension, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionType, IExtension } from 'vs/platform/extensions/common/extensions'; import { ISharedProcessService } from 'vs/platform/ipc/electron-sandbox/services'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; @@ -178,15 +178,13 @@ suite('ExtensionsListView Tests', () => { instantiationService.stub(IExtensionService, >{ onDidChangeExtensions: Event.None, - getExtensions: (): Promise => { - return Promise.resolve([ - toExtensionDescription(localEnabledTheme), - toExtensionDescription(localEnabledLanguage), - toExtensionDescription(localRandom), - toExtensionDescription(builtInTheme), - toExtensionDescription(builtInBasic) - ]); - } + extensions: [ + toExtensionDescription(localEnabledTheme), + toExtensionDescription(localEnabledLanguage), + toExtensionDescription(localRandom), + toExtensionDescription(builtInTheme), + toExtensionDescription(builtInBasic) + ] }); await (instantiationService.get(IWorkbenchExtensionEnablementService)).setEnablement([localDisabledTheme], EnablementState.DisabledGlobally); await (instantiationService.get(IWorkbenchExtensionEnablementService)).setEnablement([localDisabledLanguage], EnablementState.DisabledGlobally); @@ -380,7 +378,6 @@ suite('ExtensionsListView Tests', () => { const target = instantiationService.stubPromise(IExtensionGalleryService, 'getExtensions', workspaceRecommendedExtensions); return testableView.show('@recommended:workspace').then(result => { - assert.ok(target.calledOnce); const extensionInfos: IExtensionInfo[] = target.args[0][0]; assert.strictEqual(extensionInfos.length, workspaceRecommendedExtensions.length); assert.strictEqual(result.length, workspaceRecommendedExtensions.length); @@ -403,7 +400,6 @@ suite('ExtensionsListView Tests', () => { return testableView.show('@recommended').then(result => { const extensionInfos: IExtensionInfo[] = target.args[0][0]; - assert.ok(target.calledOnce); assert.strictEqual(extensionInfos.length, allRecommendedExtensions.length); assert.strictEqual(result.length, allRecommendedExtensions.length); for (let i = 0; i < allRecommendedExtensions.length; i++) { @@ -429,7 +425,6 @@ suite('ExtensionsListView Tests', () => { return testableView.show('@recommended:all').then(result => { const extensionInfos: IExtensionInfo[] = target.args[0][0]; - assert.ok(target.calledOnce); assert.strictEqual(extensionInfos.length, allRecommendedExtensions.length); assert.strictEqual(result.length, allRecommendedExtensions.length); for (let i = 0; i < allRecommendedExtensions.length; i++) { @@ -452,7 +447,6 @@ suite('ExtensionsListView Tests', () => { const extensionInfos: IExtensionInfo[] = queryTarget.args[0][0]; assert.ok(experimentTarget.calledOnce); - assert.ok(queryTarget.calledOnce); assert.strictEqual(extensionInfos.length, curatedList.length); assert.strictEqual(result.length, curatedList.length); for (let i = 0; i < curatedList.length; i++) { diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts index 2a1da960c22..f862b70fb53 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts @@ -51,6 +51,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { platform } from 'vs/base/common/platform'; import { arch } from 'vs/base/common/process'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; suite('ExtensionsWorkbenchServiceTest', () => { @@ -121,6 +122,12 @@ suite('ExtensionsWorkbenchServiceTest', () => { instantiationService.stub(IExtensionRecommendationsService, {}); instantiationService.stub(INotificationService, { prompt: () => null! }); + + instantiationService.stub(IExtensionService, >{ + onDidChangeExtensions: Event.None, + extensions: [], + async whenInstalledExtensionsRegistered() { return true; } + }); }); setup(async () => { @@ -439,7 +446,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { testObject = await aWorkbenchService(); const target = testObject.local[0]; - await eventToPromise(testObject.onChange); + await eventToPromise(Event.filter(testObject.onChange, e => !!e?.gallery)); assert.ok(await testObject.canInstall(target)); }); diff --git a/src/vs/workbench/contrib/externalUriOpener/common/contributedOpeners.ts b/src/vs/workbench/contrib/externalUriOpener/common/contributedOpeners.ts index af945a428c6..db0b41e39c2 100644 --- a/src/vs/workbench/contrib/externalUriOpener/common/contributedOpeners.ts +++ b/src/vs/workbench/contrib/externalUriOpener/common/contributedOpeners.ts @@ -80,7 +80,8 @@ export class ContributedExternalUriOpenersStore extends Disposable { } private async invalidateOpenersOnExtensionsChanged() { - const registeredExtensions = await this._extensionService.getExtensions(); + await this._extensionService.whenInstalledExtensionsRegistered(); + const registeredExtensions = this._extensionService.extensions; for (const [id, entry] of this._openers) { const extension = registeredExtensions.find(r => r.identifier.value === entry.extensionId); diff --git a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts index 0c7b01aa420..e76a5bfc3b2 100644 --- a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts +++ b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts @@ -70,7 +70,8 @@ class DefaultFormatter extends Disposable implements IWorkbenchContribution { } private async _updateConfigValues(): Promise { - let extensions = await this._extensionService.getExtensions(); + await this._extensionService.whenInstalledExtensionsRegistered(); + let extensions = [...this._extensionService.extensions]; extensions = extensions.sort((a, b) => { const boostA = a.categories?.find(cat => cat === 'Formatters' || cat === 'Programming Languages'); diff --git a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts index d9a9f606a1e..c329290b003 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts @@ -31,14 +31,16 @@ import { ExecutionStateCellStatusBarContrib, TimerCellStatusBarContrib } from 'v import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { ILanguageService } from 'vs/editor/common/languages/language'; -import { MenuId } from 'vs/platform/actions/common/actions'; +import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { InteractiveWindowSetting, INTERACTIVE_INPUT_CURSOR_BOUNDARY } from 'vs/workbench/contrib/interactive/browser/interactiveCommon'; import { ComplexNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOptions'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { createActionViewItem, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IAction } from 'vs/base/common/actions'; import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreventer'; import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; @@ -56,7 +58,6 @@ import { NOTEBOOK_KERNEL } from 'vs/workbench/contrib/notebook/common/notebookCo import { ICursorPositionChangedEvent } from 'vs/editor/common/cursorEvents'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { isEqual } from 'vs/base/common/resources'; -import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; const DECORATION_KEY = 'interactiveInputDecoration'; const INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'InteractiveEditorViewState'; @@ -93,6 +94,8 @@ export class InteractiveEditor extends EditorPane { #contextKeyService: IContextKeyService; #notebookKernelService: INotebookKernelService; #keybindingService: IKeybindingService; + #menuService: IMenuService; + #contextMenuService: IContextMenuService; #editorGroupService: IEditorGroupsService; #notebookExecutionStateService: INotebookExecutionStateService; #extensionService: IExtensionService; @@ -120,6 +123,8 @@ export class InteractiveEditor extends EditorPane { @ILanguageService languageService: ILanguageService, @IKeybindingService keybindingService: IKeybindingService, @IConfigurationService private configurationService: IConfigurationService, + @IMenuService menuService: IMenuService, + @IContextMenuService contextMenuService: IContextMenuService, @IEditorGroupsService editorGroupService: IEditorGroupsService, @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, @INotebookExecutionStateService notebookExecutionStateService: INotebookExecutionStateService, @@ -137,6 +142,8 @@ export class InteractiveEditor extends EditorPane { this.#notebookKernelService = notebookKernelService; this.#languageService = languageService; this.#keybindingService = keybindingService; + this.#menuService = menuService; + this.#contextMenuService = contextMenuService; this.#editorGroupService = editorGroupService; this.#notebookExecutionStateService = notebookExecutionStateService; this.#extensionService = extensionService; @@ -179,12 +186,21 @@ export class InteractiveEditor extends EditorPane { } #setupRunButtonToolbar(runButtonContainer: HTMLElement) { - this.#runbuttonToolbar = this._register(this.#instantiationService.createInstance(MenuWorkbenchToolBar, runButtonContainer, MenuId.InteractiveInputExecute, { + const menu = this._register(this.#menuService.createMenu(MenuId.InteractiveInputExecute, this.#contextKeyService)); + this.#runbuttonToolbar = this._register(new ToolBar(runButtonContainer, this.#contextMenuService, { + getKeyBinding: action => this.#keybindingService.lookupKeybinding(action.id), actionViewItemProvider: action => { return createActionViewItem(this.#instantiationService, action); }, renderDropdownAsChildElement: true })); + + const primary: IAction[] = []; + const secondary: IAction[] = []; + const result = { primary, secondary }; + + createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, result); + this.#runbuttonToolbar.setActions([...primary, ...secondary]); } #createLayoutStyles(): void { @@ -533,7 +549,9 @@ export class InteractiveEditor extends EditorPane { if (notebook && textModel) { const info = this.#notebookKernelService.getMatchingKernel(notebook); - const selectedOrSuggested = info.selected ?? info.suggestions[0]; + const selectedOrSuggested = info.selected + ?? (info.suggestions.length === 1 ? info.suggestions[0] : undefined) + ?? (info.all.length === 1 ? info.all[0] : undefined); if (selectedOrSuggested) { const language = selectedOrSuggested.supportedLanguages[0]; diff --git a/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts b/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts index 68185694831..f45f3a3c145 100644 --- a/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts +++ b/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { hookDomPurifyHrefAndSrcSanitizer } from 'vs/base/browser/dom'; +import { hookDomPurifyHrefAndSrcSanitizer, basicMarkupHtmlTags } from 'vs/base/browser/dom'; import * as dompurify from 'vs/base/browser/dompurify/dompurify'; +import { allowedMarkdownAttr } from 'vs/base/browser/markdownRenderer'; import { marked } from 'vs/base/common/marked/marked'; import { Schemas } from 'vs/base/common/network'; import { ILanguageService } from 'vs/editor/common/languages/language'; @@ -159,14 +160,14 @@ function sanitize(documentContent: string, allowUnknownProtocols: boolean): stri return dompurify.sanitize(documentContent, { ...{ ALLOWED_TAGS: [ - 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'h7', 'h8', 'br', 'b', 'i', 'strong', 'em', 'a', 'pre', 'code', 'img', 'tt', - 'div', 'ins', 'del', 'sup', 'sub', 'p', 'ol', 'ul', 'table', 'thead', 'tbody', 'tfoot', 'blockquote', 'dl', 'dt', - 'dd', 'kbd', 'q', 'samp', 'var', 'hr', 'ruby', 'rt', 'rp', 'li', 'tr', 'td', 'th', 's', 'strike', 'summary', 'details', - 'caption', 'figure', 'figcaption', 'abbr', 'bdo', 'cite', 'dfn', 'mark', 'small', 'span', 'time', 'wbr', 'checkbox', 'checklist', 'vertically-centered' + ...basicMarkupHtmlTags, + 'checkbox', + 'checklist', ], ALLOWED_ATTR: [ - 'href', 'data-href', 'data-command', 'target', 'title', 'name', 'src', 'alt', 'class', 'id', 'role', 'tabindex', 'style', 'data-code', - 'width', 'height', 'align', 'x-dispatch', + ...allowedMarkdownAttr, + 'data-command', 'name', 'id', 'role', 'tabindex', + 'x-dispatch', 'required', 'checked', 'placeholder', 'when-checked', 'checked-on', ], }, diff --git a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts index 8e2dc6ad355..dd7c776f7db 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts +++ b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts @@ -603,15 +603,6 @@ export class MarkerViewModel extends Disposable { this.setQuickFixes(true); } - showQuickfixes(): void { - this.setQuickFixes(false).then(() => this.quickFixAction.run()); - } - - async getQuickFixes(waitForModel: boolean): Promise { - const codeActions = await this.getCodeActions(waitForModel); - return codeActions ? this.toActions(codeActions) : []; - } - private async setQuickFixes(waitForModel: boolean): Promise { const codeActions = await this.getCodeActions(waitForModel); this.quickFixAction.quickFixes = codeActions ? this.toActions(codeActions) : []; diff --git a/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts b/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts index 73e849d5fa8..95f83325e3f 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts @@ -8,14 +8,16 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { ILocalizedString } from 'vs/platform/action/common/action'; import { Action2, IAction2Options, MenuId } from 'vs/platform/actions/common/actions'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IResourceMergeEditorInput } from 'vs/workbench/common/editor'; -import { MergeEditorInputData } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; +import { IEditorIdentifier, IResourceMergeEditorInput } from 'vs/workbench/common/editor'; +import { MergeEditorInput, MergeEditorInputData } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; +import { IMergeEditorInputModel } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel'; import { MergeEditor } from 'vs/workbench/contrib/mergeEditor/browser/view/mergeEditor'; import { MergeEditorViewModel } from 'vs/workbench/contrib/mergeEditor/browser/view/viewModel'; -import { ctxIsMergeEditor, ctxMergeEditorLayout, ctxMergeEditorShowBase } from 'vs/workbench/contrib/mergeEditor/common/mergeEditor'; +import { ctxIsMergeEditor, ctxMergeEditorLayout, ctxMergeEditorShowBase, ctxMergeEditorShowNonConflictingChanges } from 'vs/workbench/contrib/mergeEditor/common/mergeEditor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; abstract class MergeEditorAction extends Action2 { @@ -37,6 +39,41 @@ abstract class MergeEditorAction extends Action2 { abstract runWithViewModel(viewModel: MergeEditorViewModel, accessor: ServicesAccessor): void; } +interface MergeEditorAction2Args { + inputModel: IMergeEditorInputModel; + viewModel: MergeEditorViewModel; + input: MergeEditorInput; + editorIdentifier: IEditorIdentifier; +} + +abstract class MergeEditorAction2 extends Action2 { + constructor(desc: Readonly) { + super(desc); + } + + override run(accessor: ServicesAccessor, ...args: any[]): void { + const { activeEditorPane } = accessor.get(IEditorService); + if (activeEditorPane instanceof MergeEditor) { + const vm = activeEditorPane.viewModel.get(); + if (!vm) { + return; + } + + return this.runWithMergeEditor({ + viewModel: vm, + inputModel: activeEditorPane.inputModel.get()!, + input: activeEditorPane.input as MergeEditorInput, + editorIdentifier: { + editor: activeEditorPane.input, + groupId: activeEditorPane.group.id, + } + }, accessor, ...args) as any; + } + } + + abstract runWithMergeEditor(context: MergeEditorAction2Args, accessor: ServicesAccessor, ...args: any[]): unknown; +} + export class OpenMergeEditor extends Action2 { constructor() { super({ @@ -181,6 +218,35 @@ export class SetColumnLayout extends Action2 { } } +export class ShowNonConflictingChanges extends Action2 { + constructor() { + super({ + id: 'merge.showNonConflictingChanges', + title: { + value: localize('showNonConflictingChanges', 'Show Non-Conflicting Changes'), + original: 'Show Non-Conflicting Changes', + }, + toggled: ctxMergeEditorShowNonConflictingChanges.isEqualTo(true), + menu: [ + { + id: MenuId.EditorTitle, + when: ctxIsMergeEditor, + group: '3_merge', + order: 9, + }, + ], + precondition: ctxIsMergeEditor, + }); + } + + run(accessor: ServicesAccessor): void { + const { activeEditorPane } = accessor.get(IEditorService); + if (activeEditorPane instanceof MergeEditor) { + activeEditorPane.toggleShowNonConflictingChanges(); + } + } +} + export class ShowHideBase extends Action2 { constructor() { super({ @@ -395,6 +461,9 @@ export class CompareInput2WithBaseCommand extends MergeEditorAction { } async function mergeEditorCompare(viewModel: MergeEditorViewModel, editorService: IEditorService, inputNumber: 1 | 2) { + + editorService.openEditor(editorService.activeEditor!, { pinned: true }); + const model = viewModel.model; const base = model.base; const input = inputNumber === 1 ? viewModel.inputCodeEditorView1.editor : viewModel.inputCodeEditorView2.editor; @@ -531,3 +600,49 @@ export class ResetDirtyConflictsToBaseCommand extends MergeEditorAction { viewModel.model.resetDirtyConflictsToBase(); } } + +// this is an API command +export class AcceptMerge extends MergeEditorAction2 { + constructor() { + super({ + id: 'mergeEditor.acceptMerge', + category: mergeEditorCategory, + title: { + value: localize( + 'mergeEditor.acceptMerge', + 'Accept Merge' + ), + original: 'Accept Merge', + }, + f1: false, + precondition: ctxIsMergeEditor + }); + } + + override async runWithMergeEditor({ inputModel, editorIdentifier, viewModel }: MergeEditorAction2Args, accessor: ServicesAccessor) { + const dialogService = accessor.get(IDialogService); + const editorService = accessor.get(IEditorService); + + if (viewModel.model.unhandledConflictsCount.get() > 0) { + const confirmResult = await dialogService.confirm({ + type: 'info', + message: localize('mergeEditor.acceptMerge.unhandledConflicts', "There are still unhandled conflicts. Are you sure you want to accept the merge?"), + primaryButton: localize('mergeEditor.acceptMerge.unhandledConflicts.accept', "Accept merge with unhandled conflicts"), + secondaryButton: localize('mergeEditor.acceptMerge.unhandledConflicts.cancel', "Cancel"), + }); + + if (!confirmResult.confirmed) { + return { + successful: false + }; + } + } + + await inputModel.accept(); + await editorService.closeEditor(editorIdentifier); + + return { + successful: true + }; + } +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts index 5b426fa182d..0e8385ea1d0 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts @@ -11,10 +11,10 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { EditorExtensions, IEditorFactoryRegistry } from 'vs/workbench/common/editor'; -import { AcceptAllInput1, AcceptAllInput2, CompareInput1WithBaseCommand, CompareInput2WithBaseCommand, GoToNextUnhandledConflict, GoToPreviousUnhandledConflict, OpenBaseFile, OpenMergeEditor, OpenResultResource, ResetDirtyConflictsToBaseCommand, ResetToBaseAndAutoMergeCommand, SetColumnLayout, SetMixedLayout, ShowHideBase, ToggleActiveConflictInput1, ToggleActiveConflictInput2 } from 'vs/workbench/contrib/mergeEditor/browser/commands/commands'; -import { MergeEditorCopyContentsToJSON, MergeEditorSaveContentsToFolder, MergeEditorLoadContentsFromFolder } from 'vs/workbench/contrib/mergeEditor/browser/commands/devCommands'; +import { AcceptAllInput1, AcceptAllInput2, AcceptMerge, CompareInput1WithBaseCommand, CompareInput2WithBaseCommand, GoToNextUnhandledConflict, GoToPreviousUnhandledConflict, OpenBaseFile, OpenMergeEditor, OpenResultResource, ResetDirtyConflictsToBaseCommand, ResetToBaseAndAutoMergeCommand, SetColumnLayout, SetMixedLayout, ShowHideBase, ShowNonConflictingChanges, ToggleActiveConflictInput1, ToggleActiveConflictInput2 } from 'vs/workbench/contrib/mergeEditor/browser/commands/commands'; +import { MergeEditorCopyContentsToJSON, MergeEditorLoadContentsFromFolder, MergeEditorSaveContentsToFolder } from 'vs/workbench/contrib/mergeEditor/browser/commands/devCommands'; import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; -import { MergeEditor, MergeEditorResolverContribution, MergeEditorOpenHandlerContribution } from 'vs/workbench/contrib/mergeEditor/browser/view/mergeEditor'; +import { MergeEditor, MergeEditorOpenHandlerContribution, MergeEditorResolverContribution } from 'vs/workbench/contrib/mergeEditor/browser/view/mergeEditor'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { MergeEditorSerializer } from './mergeEditorSerializer'; @@ -54,6 +54,7 @@ registerAction2(SetColumnLayout); registerAction2(ShowHideBase); registerAction2(OpenMergeEditor); registerAction2(OpenBaseFile); +registerAction2(ShowNonConflictingChanges); registerAction2(GoToNextUnhandledConflict); registerAction2(GoToPreviousUnhandledConflict); @@ -70,6 +71,8 @@ registerAction2(AcceptAllInput2); registerAction2(ResetToBaseAndAutoMergeCommand); registerAction2(ResetDirtyConflictsToBaseCommand); +registerAction2(AcceptMerge); + // Dev Commands registerAction2(MergeEditorCopyContentsToJSON); registerAction2(MergeEditorSaveContentsToFolder); @@ -82,3 +85,23 @@ Registry Registry .as(WorkbenchExtensions.Workbench) .registerWorkbenchContribution(MergeEditorResolverContribution, 'MergeEditorResolverContribution', LifecyclePhase.Starting); +/* +class MergeEditorWorkbenchContribution extends Disposable implements IWorkbenchContribution { + constructor(@IWorkingCopyEditorService private readonly _workingCopyEditorService: IWorkingCopyEditorService) { + super(); + + this._register( + _workingCopyEditorService.registerHandler({ + createEditor(workingCopy) { + throw new BugIndicatingError('not supported'); + }, + handles(workingCopy) { + return workingCopy.typeId === ''; + }, + isOpen(workingCopy, editor) { + return workingCopy.resource.toString() === that._model?.resultTextModel.uri.toString(); + }, + })); + } +} +*/ diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts index dfafb936be0..6f41a7f3632 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts @@ -3,26 +3,22 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { basename, isEqual } from 'vs/base/common/resources'; -import Severity from 'vs/base/common/severity'; +import { assertFn } from 'vs/base/common/assert'; +import { autorun } from 'vs/base/common/observable'; +import { isEqual } from 'vs/base/common/resources'; +import { isDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { localize } from 'vs/nls'; -import { ConfirmResult, IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; -import { DEFAULT_EDITOR_ASSOCIATION, EditorInputCapabilities, IEditorIdentifier, IResourceMergeEditorInput, isResourceMergeEditorInput, IUntypedEditorInput } from 'vs/workbench/common/editor'; +import { DEFAULT_EDITOR_ASSOCIATION, EditorInputCapabilities, IResourceMergeEditorInput, IRevertOptions, isResourceMergeEditorInput, IUntypedEditorInput } from 'vs/workbench/common/editor'; import { EditorInput, IEditorCloseHandler } from 'vs/workbench/common/editor/editorInput'; import { AbstractTextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; -import { MergeDiffComputer } from 'vs/workbench/contrib/mergeEditor/browser/model/diffComputer'; -import { InputData, MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel'; +import { IMergeEditorInputModel, TempFileMergeEditorModeFactory, WorkspaceMergeEditorModeFactory } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { ILanguageSupport, ITextFileEditorModel, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { autorun } from 'vs/base/common/observable'; -import { WorkerBasedDocumentDiffProvider } from 'vs/editor/browser/widget/workerBasedDocumentDiffProvider'; -import { ProjectedDiffComputer } from 'vs/workbench/contrib/mergeEditor/browser/model/projectedDocumentDiffProvider'; +import { ILanguageSupport, ITextFileSaveOptions, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; export class MergeEditorInputData { constructor( @@ -34,13 +30,22 @@ export class MergeEditorInputData { } export class MergeEditorInput extends AbstractTextResourceEditorInput implements ILanguageSupport { - static readonly ID = 'mergeEditor.Input'; - private _model?: MergeEditorModel; - private _outTextModel?: ITextFileEditorModel; + private _inputModel?: IMergeEditorInputModel; - override closeHandler: MergeEditorCloseHandler | undefined; + override closeHandler: IEditorCloseHandler = { + showConfirm: () => this._inputModel?.shouldConfirmClose() ?? false, + confirm: async (editors) => { + assertFn(() => editors.every(e => e.editor instanceof MergeEditorInput)); + const inputModels = editors.map(e => (e.editor as MergeEditorInput)._inputModel).filter(isDefined); + return await this._inputModel!.confirmClose(inputModels); + }, + }; + + private get useWorkingCopy() { + return this.configurationService.getValue('mergeEditor.useWorkingCopy') ?? false; + } constructor( public readonly base: URI, @@ -48,34 +53,13 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements public readonly input2: MergeEditorInputData, public readonly result: URI, @IInstantiationService private readonly _instaService: IInstantiationService, - @ITextModelService private readonly _textModelService: ITextModelService, @IEditorService editorService: IEditorService, @ITextFileService textFileService: ITextFileService, @ILabelService labelService: ILabelService, - @IFileService fileService: IFileService + @IFileService fileService: IFileService, + @IConfigurationService private readonly configurationService: IConfigurationService, ) { super(result, undefined, editorService, textFileService, labelService, fileService); - - const modelListener = new DisposableStore(); - const handleDidCreate = (model: ITextFileEditorModel) => { - // TODO@jrieken copied from fileEditorInput.ts - if (isEqual(result, model.resource)) { - modelListener.clear(); - this._outTextModel = model; - modelListener.add(model.onDidChangeDirty(() => this._onDidChangeDirty.fire())); - modelListener.add(model.onDidSaveError(() => this._onDidChangeDirty.fire())); - - modelListener.add(model.onDidChangeReadonly(() => this._onDidChangeCapabilities.fire())); - - modelListener.add(model.onWillDispose(() => { - this._outTextModel = undefined; - modelListener.clear(); - })); - } - }; - textFileService.files.onDidCreate(handleDidCreate, this, modelListener); - textFileService.files.models.forEach(handleDidCreate); - this._store.add(modelListener); } override dispose(): void { @@ -91,68 +75,53 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements } override get capabilities(): EditorInputCapabilities { - return super.capabilities | EditorInputCapabilities.MultipleEditors; + let capabilities = super.capabilities | EditorInputCapabilities.MultipleEditors; + + if (this.useWorkingCopy) { + capabilities |= EditorInputCapabilities.Untitled; + } + + return capabilities; } override getName(): string { return localize('name', "Merging: {0}", super.getName()); } - override async resolve(): Promise { - if (!this._model) { - const toInputData = async (data: MergeEditorInputData): Promise => { - const ref = await this._textModelService.createModelReference(data.uri); - this._store.add(ref); - return { - textModel: ref.object.textEditorModel, - title: data.title, - description: data.description, - detail: data.detail, - }; - }; + private readonly mergeEditorModeFactory = this._instaService.createInstance( + this.useWorkingCopy + ? TempFileMergeEditorModeFactory + : WorkspaceMergeEditorModeFactory + ); - const [ - base, - result, - input1Data, - input2Data, - ] = await Promise.all([ - this._textModelService.createModelReference(this.base), - this._textModelService.createModelReference(this.result), - toInputData(this.input1), - toInputData(this.input2), - ]); + override async resolve(): Promise { + if (!this._inputModel) { + const inputModel = this._register(await this.mergeEditorModeFactory.createInputModel({ + base: this.base, + input1: this.input1, + input2: this.input2, + result: this.result, + })); + this._inputModel = inputModel; - this._store.add(base); - this._store.add(result); - - const diffProvider = this._instaService.createInstance(WorkerBasedDocumentDiffProvider); - this._model = this._instaService.createInstance( - MergeEditorModel, - base.object.textEditorModel, - input1Data, - input2Data, - result.object.textEditorModel, - this._instaService.createInstance(MergeDiffComputer, diffProvider), - this._instaService.createInstance(MergeDiffComputer, this._instaService.createInstance(ProjectedDiffComputer, diffProvider)), - { - resetUnknownOnInitialization: false - }, - ); - this._store.add(this._model); - - // set/unset the closeHandler whenever unhandled conflicts are detected - const closeHandler = this._instaService.createInstance(MergeEditorCloseHandler, this._model); - this._store.add(autorun('closeHandler', reader => { - const value = this._model!.hasUnhandledConflicts.read(reader); - this.closeHandler = value ? closeHandler : undefined; + this._register(autorun('fire dirty event', (reader) => { + inputModel.isDirty.read(reader); + this._onDidChangeDirty.fire(); })); - await this._model.onInitialized; - + await this._inputModel.model.onInitialized; } - return this._model; + return this._inputModel; + } + + public async accept(): Promise { + await this._inputModel?.accept(); + } + + override async save(group: number, options?: ITextFileSaveOptions | undefined): Promise { + await this._inputModel?.save(options); + return undefined; } override toUntyped(): IResourceMergeEditorInput { @@ -188,125 +157,20 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements return false; } + override async revert(group: number, options?: IRevertOptions): Promise { + return this._inputModel?.revert(options); + } + // ---- FileEditorInput override isDirty(): boolean { - return Boolean(this._outTextModel?.isDirty()); + return this._inputModel?.isDirty.get() ?? false; } setLanguageId(languageId: string, source?: string): void { - this._model?.setLanguageId(languageId, source); + this._inputModel?.model.setLanguageId(languageId, source); } // implement get/set languageId // implement get/set encoding } - -class MergeEditorCloseHandler implements IEditorCloseHandler { - - private _ignoreUnhandledConflicts: boolean = false; - - constructor( - private readonly _model: MergeEditorModel, - @IDialogService private readonly _dialogService: IDialogService, - ) { } - - showConfirm(): boolean { - // unhandled conflicts -> 3wm asks to confirm UNLESS we explicitly set this input - // to ignore unhandled conflicts. This happens only after confirming to ignore unhandled changes - return !this._ignoreUnhandledConflicts && this._model.hasUnhandledConflicts.get(); - } - - async confirm(editors: readonly IEditorIdentifier[]): Promise { - - const handler: MergeEditorCloseHandler[] = []; - let someAreDirty = false; - - for (const { editor } of editors) { - if (editor.closeHandler instanceof MergeEditorCloseHandler && editor.closeHandler._model.hasUnhandledConflicts.get()) { - handler.push(editor.closeHandler); - someAreDirty = someAreDirty || editor.isDirty(); - } - } - - if (handler.length === 0) { - // shouldn't happen - return ConfirmResult.SAVE; - } - - const result = someAreDirty - ? await this._confirmDirty(handler) - : await this._confirmNoneDirty(handler); - - if (result !== ConfirmResult.CANCEL) { - // save or ignore: in both cases we tell the inputs to ignore unhandled conflicts - // for the dirty state computation. - for (const input of handler) { - input._ignoreUnhandledConflicts = true; - } - } - - return result; - } - - private async _confirmDirty(handler: MergeEditorCloseHandler[]): Promise { - const isMany = handler.length > 1; - - const message = isMany - ? localize('messageN', 'Do you want to save the changes you made to {0} files?', handler.length) - : localize('message1', 'Do you want to save the changes you made to {0}?', basename(handler[0]._model.resultTextModel.uri)); - - const options = { - cancelId: 2, - detail: isMany - ? localize('detailN', "The files contain unhandled conflicts. Your changes will be lost if you don't save them.") - : localize('detail1', "The file contains unhandled conflicts. Your changes will be lost if you don't save them.") - }; - - const actions: string[] = [ - localize('saveWithConflict', "Save with Conflicts"), - localize('discard', "Don't Save"), - localize('cancel', "Cancel"), - ]; - - const { choice } = await this._dialogService.show(Severity.Info, message, actions, options); - - if (choice === options.cancelId) { - // cancel: stay in editor - return ConfirmResult.CANCEL; - } else if (choice === 0) { - // save with conflicts - return ConfirmResult.SAVE; - } else { - // discard changes - return ConfirmResult.DONT_SAVE; - } - } - - private async _confirmNoneDirty(handler: MergeEditorCloseHandler[]): Promise { - const isMany = handler.length > 1; - - const message = isMany - ? localize('conflictN', 'Do you want to close with conflicts in {0} files?', handler.length) - : localize('conflict1', 'Do you want to close with conflicts in {0}?', basename(handler[0]._model.resultTextModel.uri)); - - const options = { - cancelId: 1, - detail: isMany - ? localize('detailNotDirtyN', "The files contain unhandled conflicts.") - : localize('detailNotDirty1', "The file contains unhandled conflicts.") - }; - - const actions = [ - localize('closeWithConflicts', "Close with Conflicts"), - localize('cancel', "Cancel"), - ]; - - const { choice } = await this._dialogService.show(Severity.Info, message, actions, options); - if (choice === options.cancelId) { - return ConfirmResult.CANCEL; - } else { - return ConfirmResult.SAVE; - } - } -} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel.ts new file mode 100644 index 00000000000..4ef67a9d65a --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel.ts @@ -0,0 +1,447 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { assertFn } from 'vs/base/common/assert'; +import { BugIndicatingError } from 'vs/base/common/errors'; +import { Event } from 'vs/base/common/event'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { derived, IObservable, observableFromEvent, observableValue } from 'vs/base/common/observable'; +import { basename, isEqual } from 'vs/base/common/resources'; +import Severity from 'vs/base/common/severity'; +import { URI } from 'vs/base/common/uri'; +import { WorkerBasedDocumentDiffProvider } from 'vs/editor/browser/widget/workerBasedDocumentDiffProvider'; +import { IModelService } from 'vs/editor/common/services/model'; +import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; +import { localize } from 'vs/nls'; +import { ConfirmResult, IDialogOptions, IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IEditorModel } from 'vs/platform/editor/common/editor'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IRevertOptions } from 'vs/workbench/common/editor'; +import { EditorModel } from 'vs/workbench/common/editor/editorModel'; +import { MergeEditorInputData } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; +import { MergeDiffComputer } from 'vs/workbench/contrib/mergeEditor/browser/model/diffComputer'; +import { InputData, MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel'; +import { ProjectedDiffComputer } from 'vs/workbench/contrib/mergeEditor/browser/model/projectedDocumentDiffProvider'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ITextFileEditorModel, ITextFileSaveOptions, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; + +export interface MergeEditorArgs { + base: URI; + input1: MergeEditorInputData; + input2: MergeEditorInputData; + result: URI; +} + +export interface IMergeEditorInputModelFactory { + createInputModel(args: MergeEditorArgs): Promise; +} + +export interface IMergeEditorInputModel extends IDisposable, IEditorModel { + readonly resultUri: URI; + + readonly model: MergeEditorModel; + readonly isDirty: IObservable; + + save(options?: ITextFileSaveOptions): Promise; + + /** + * If save resets the dirty state, revert must do so too. + */ + revert(options?: IRevertOptions): Promise; + + shouldConfirmClose(): boolean; + + confirmClose(inputModels: IMergeEditorInputModel[]): Promise; + + /** + * Marks the merge as done. The merge editor must be closed afterwards. + */ + accept(): Promise; +} + +/* ================ Temp File ================ */ + +export class TempFileMergeEditorModeFactory implements IMergeEditorInputModelFactory { + constructor( + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ITextModelService private readonly _textModelService: ITextModelService, + @IModelService private readonly _modelService: IModelService, + ) { + } + + async createInputModel(args: MergeEditorArgs): Promise { + const store = new DisposableStore(); + + const [ + base, + result, + input1Data, + input2Data, + ] = await Promise.all([ + this._textModelService.createModelReference(args.base), + this._textModelService.createModelReference(args.result), + toInputData(args.input1, this._textModelService, store), + toInputData(args.input2, this._textModelService, store), + ]); + + store.add(base); + store.add(result); + + const tempResultUri = result.object.textEditorModel.uri.with({ scheme: 'merge-result' }); + + const temporaryResultModel = this._modelService.createModel( + '', + { + languageId: result.object.textEditorModel.getLanguageId(), + onDidChange: Event.None, + }, + tempResultUri, + ); + store.add(temporaryResultModel); + + const diffProvider = this._instantiationService.createInstance(WorkerBasedDocumentDiffProvider); + const model = this._instantiationService.createInstance( + MergeEditorModel, + base.object.textEditorModel, + input1Data, + input2Data, + temporaryResultModel, + this._instantiationService.createInstance(MergeDiffComputer, diffProvider), + this._instantiationService.createInstance(MergeDiffComputer, this._instantiationService.createInstance(ProjectedDiffComputer, diffProvider)), + { + resetResult: true, + } + ); + store.add(model); + + await model.onInitialized; + + return this._instantiationService.createInstance(TempFileMergeEditorInputModel, model, store, result.object, args.result); + } +} + +class TempFileMergeEditorInputModel extends EditorModel implements IMergeEditorInputModel { + private readonly savedAltVersionId = observableValue('initialAltVersionId', this.model.resultTextModel.getAlternativeVersionId()); + private readonly altVersionId = observableFromEvent( + e => this.model.resultTextModel.onDidChangeContent(e), + () => + /** @description getAlternativeVersionId */ this.model.resultTextModel.getAlternativeVersionId() + ); + + public readonly isDirty = derived( + 'isDirty', + (reader) => this.altVersionId.read(reader) !== this.savedAltVersionId.read(reader) + ); + + private finished = false; + + constructor( + public readonly model: MergeEditorModel, + private readonly disposable: IDisposable, + private readonly result: IResolvedTextEditorModel, + public readonly resultUri: URI, + @ITextFileService private readonly textFileService: ITextFileService, + @IDialogService private readonly dialogService: IDialogService, + @IEditorService private readonly editorService: IEditorService, + ) { + super(); + } + + override dispose(): void { + this.disposable.dispose(); + super.dispose(); + } + + async accept(): Promise { + const value = await this.model.resultTextModel.getValue(); + this.result.textEditorModel.setValue(value); + this.savedAltVersionId.set(this.model.resultTextModel.getAlternativeVersionId(), undefined); + await this.textFileService.save(this.result.textEditorModel.uri); + this.finished = true; + } + + private async _discard(): Promise { + await this.textFileService.revert(this.model.resultTextModel.uri); + this.savedAltVersionId.set(this.model.resultTextModel.getAlternativeVersionId(), undefined); + this.finished = true; + } + + public shouldConfirmClose(): boolean { + return true; + } + + public async confirmClose(inputModels: TempFileMergeEditorInputModel[]): Promise { + assertFn( + () => inputModels.some((m) => m === this) + ); + + const someDirty = inputModels.some((m) => m.isDirty.get()); + let choice: number; + if (someDirty) { + const isMany = inputModels.length > 1; + + const message = isMany + ? localize('messageN', 'Do you want keep the merge result of {0} files?', inputModels.length) + : localize('message1', 'Do you want keep the merge result of {0}?', basename(inputModels[0].model.resultTextModel.uri)); + + const hasUnhandledConflicts = inputModels.some((m) => m.model.hasUnhandledConflicts.get()); + + const options: IDialogOptions = { + cancelId: 2, + detail: + hasUnhandledConflicts + ? isMany + ? localize('detailNConflicts', "The files contain unhandled conflicts. The merge results will be lost if you don't save them.") + : localize('detail1Conflicts', "The file contains unhandled conflicts. The merge result will be lost if you don't save it.") + : isMany + ? localize('detailN', "The merge results will be lost if you don't save them.") + : localize('detail1', "The merge result will be lost if you don't save it.") + }; + + const actions: string[] = [ + hasUnhandledConflicts ? localize('saveWithConflict', "Save With Conflicts") : localize('save', "Save"), + localize('discard', "Don't Save"), + localize('cancel', "Cancel"), + ]; + + choice = (await this.dialogService.show(Severity.Info, message, actions, options)).choice; + } else { + choice = 1; + } + + if (choice === 2) { + // cancel: stay in editor + return ConfirmResult.CANCEL; + } else if (choice === 0) { + // save with conflicts + await Promise.all(inputModels.map(m => m.accept())); + return ConfirmResult.SAVE; // Save is a no-op anyway + } else { + // discard changes + await Promise.all(inputModels.map(m => m._discard())); + return ConfirmResult.DONT_SAVE; // Revert is a no-op + } + } + + public async save(options?: ITextFileSaveOptions): Promise { + if (this.finished) { + return; + } + // It does not make sense to save anything in the temp file mode. + // The file stays dirty from the first edit on. + + (async () => { + const result = await this.dialogService.show( + Severity.Info, + localize( + 'saveTempFile', + "Do you want to accept the merge result? This will write the merge result to the original file and close the merge editor." + ), + [ + localize('acceptMerge', 'Accept Merge'), + localize('cancel', "Cancel"), + ], + { cancelId: 1 } + ); + + if (result.choice === 0) { + await this.accept(); + const editors = this.editorService.findEditors(this.resultUri).filter(e => e.editor.typeId === 'mergeEditor.Input'); + await this.editorService.closeEditors(editors); + } + })(); + } + + public async revert(options?: IRevertOptions): Promise { + // no op + } +} + +/* ================ Workspace ================ */ + +export class WorkspaceMergeEditorModeFactory implements IMergeEditorInputModelFactory { + constructor( + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ITextModelService private readonly _textModelService: ITextModelService, + @ITextFileService private readonly textFileService: ITextFileService, + ) { + } + + public async createInputModel(args: MergeEditorArgs): Promise { + const store = new DisposableStore(); + + let resultTextFileModel = undefined as ITextFileEditorModel | undefined; + const modelListener = store.add(new DisposableStore()); + const handleDidCreate = (model: ITextFileEditorModel) => { + if (isEqual(args.result, model.resource)) { + modelListener.clear(); + resultTextFileModel = model; + } + }; + modelListener.add(this.textFileService.files.onDidCreate(handleDidCreate)); + this.textFileService.files.models.forEach(handleDidCreate); + + const [ + base, + result, + input1Data, + input2Data, + ] = await Promise.all([ + this._textModelService.createModelReference(args.base), + this._textModelService.createModelReference(args.result), + toInputData(args.input1, this._textModelService, store), + toInputData(args.input2, this._textModelService, store), + ]); + + store.add(base); + store.add(result); + + if (!resultTextFileModel) { + throw new BugIndicatingError(); + } + // So that "Don't save" does revert the file + await resultTextFileModel.save(); + + const diffProvider = this._instantiationService.createInstance(WorkerBasedDocumentDiffProvider); + const model = this._instantiationService.createInstance( + MergeEditorModel, + base.object.textEditorModel, + input1Data, + input2Data, + result.object.textEditorModel, + this._instantiationService.createInstance(MergeDiffComputer, diffProvider), + this._instantiationService.createInstance(MergeDiffComputer, this._instantiationService.createInstance(ProjectedDiffComputer, diffProvider)), + { + resetResult: true + } + ); + store.add(model); + + return this._instantiationService.createInstance(WorkspaceMergeEditorInputModel, model, store, resultTextFileModel); + } +} + +class WorkspaceMergeEditorInputModel extends EditorModel implements IMergeEditorInputModel { + public readonly isDirty = observableFromEvent( + Event.any(this.resultTextFileModel.onDidChangeDirty, this.resultTextFileModel.onDidSaveError), + () => /** @description isDirty */ this.resultTextFileModel.isDirty() + ); + + constructor( + public readonly model: MergeEditorModel, + private readonly disposableStore: DisposableStore, + private readonly resultTextFileModel: ITextFileEditorModel, + @IDialogService private readonly _dialogService: IDialogService, + ) { + super(); + } + + public override dispose(): void { + this.disposableStore.dispose(); + super.dispose(); + } + + public async accept(): Promise { + await this.resultTextFileModel.save(); + } + + get resultUri(): URI { + return this.resultTextFileModel.resource; + } + + async save(options?: ITextFileSaveOptions): Promise { + await this.resultTextFileModel.save(options); + } + + /** + * If save resets the dirty state, revert must do so too. + */ + async revert(options?: IRevertOptions): Promise { + await this.resultTextFileModel.revert(options); + } + + shouldConfirmClose(): boolean { + // Always confirm + return true; + //return this.resultTextFileModel.isDirty(); + } + + async confirmClose(inputModels: IMergeEditorInputModel[]): Promise { + const isMany = inputModels.length > 1; + const someDirty = inputModels.some(m => m.isDirty.get()); + const someUnhandledConflicts = inputModels.some(m => m.model.hasUnhandledConflicts.get()); + if (someDirty) { + const message = isMany + ? localize('workspace.messageN', 'Do you want to save the changes you made to {0} files?', inputModels.length) + : localize('workspace.message1', 'Do you want to save the changes you made to {0}?', basename(inputModels[0].resultUri)); + const options: IDialogOptions = { + detail: + someUnhandledConflicts ? + isMany + ? localize('workspace.detailN.unhandled', "The files contain unhandled conflicts. Your changes will be lost if you don't save them.") + : localize('workspace.detail1.unhandled', "The file contains unhandled conflicts. Your changes will be lost if you don't save them.") + : isMany + ? localize('workspace.detailN.handled', "Your changes will be lost if you don't save them.") + : localize('workspace.detail1.handled', "Your changes will be lost if you don't save them.") + }; + const actions: [string, ConfirmResult][] = [ + [ + someUnhandledConflicts + ? localize('workspace.saveWithConflict', 'Save with Conflicts') + : localize('workspace.save', 'Save'), + ConfirmResult.SAVE, + ], + [localize('workspace.doNotSave', "Don't Save"), ConfirmResult.DONT_SAVE], + // TODO [localize('workspace.discard', "Discard changes"), ConfirmResult.DONT_SAVE], + [localize('workspace.cancel', 'Cancel'), ConfirmResult.CANCEL], + ]; + + const { choice } = await this._dialogService.show(Severity.Info, message, actions.map(a => a[0]), { ...options, cancelId: actions.length - 1 }); + return actions[choice][1]; + + } else if (someUnhandledConflicts) { + const message = isMany + ? localize('workspace.messageN.nonDirty', 'Do you want to close {0} merge editors?', inputModels.length) + : localize('workspace.message1.nonDirty', 'Do you want to close the merge editor for {0}?', basename(inputModels[0].resultUri)); + const options: IDialogOptions = { + detail: + someUnhandledConflicts ? + isMany + ? localize('workspace.detailN.unhandled.nonDirty', "The files contain unhandled conflicts.") + : localize('workspace.detail1.unhandled.nonDirty', "The file contains unhandled conflicts.") + : undefined + }; + const actions: [string, ConfirmResult][] = [ + [ + someUnhandledConflicts + ? localize('workspace.closeWithConflicts', 'Close with Conflicts') + : localize('workspace.close', 'Close'), + ConfirmResult.SAVE, + ], + // TODO [localize('workspace.discard', "Discard changes"), ConfirmResult.DONT_SAVE], + [localize('workspace.cancel', 'Cancel'), ConfirmResult.CANCEL], + ]; + + const { choice } = await this._dialogService.show(Severity.Info, message, actions.map(a => a[0]), { ...options, cancelId: actions.length - 1 }); + return actions[choice][1]; + } else { + // This shouldn't do anything + return ConfirmResult.SAVE; + } + } +} + +/* ================= Utils ================== */ + +async function toInputData(data: MergeEditorInputData, textModelService: ITextModelService, store: DisposableStore): Promise { + const ref = await textModelService.createModelReference(data.uri); + store.add(ref); + return { + textModel: ref.object.textEditorModel, + title: data.title, + description: data.description, + detail: data.detail, + }; +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts index 1cab4aeb9a2..d6fc05da492 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts @@ -47,6 +47,10 @@ export class MergeDiffComputer implements IMergeDiffComputer { } ); + if (textModel1.isDisposed() || textModel2.isDisposed()) { + return { diffs: null }; + } + const changes = result.changes.map(c => new DetailedLineRangeMapping( toLineRange(c.originalRange), diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts index 357ffdfa1fb..365d11269b8 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts @@ -58,9 +58,9 @@ export class MergeEditorModel extends EditorModel { readonly resultTextModel: ITextModel, private readonly diffComputer: IMergeDiffComputer, private readonly diffComputerConflictProjection: IMergeDiffComputer, - options: { resetUnknownOnInitialization: boolean }, + private readonly options: { resetResult: boolean }, @IModelService private readonly modelService: IModelService, - @ILanguageService private readonly languageService: ILanguageService + @ILanguageService private readonly languageService: ILanguageService, ) { super(); @@ -68,51 +68,122 @@ export class MergeEditorModel extends EditorModel { this._register(keepAlive(this.input1ResultMapping)); this._register(keepAlive(this.input2ResultMapping)); - let shouldRecomputeHandledFromAccepted = true; - this._register( - autorunHandleChanges( - 'Merge Editor Model: Recompute State From Result', - { - handleChange: (ctx) => { - if (ctx.didChange(this.modifiedBaseRangeResultStates)) { - shouldRecomputeHandledFromAccepted = true; - } - return ctx.didChange(this.resultTextModelDiffs.diffs) - // Ignore non-text changes as we update the state directly - ? ctx.change === TextModelDiffChangeReason.textChange - : true; - }, - }, - (reader) => { - const states = this.modifiedBaseRangeResultStates.read(reader); - if (!this.isUpToDate.read(reader)) { - return; - } - const resultDiffs = this.resultTextModelDiffs.diffs.read(reader); - transaction(tx => { - /** @description Merge Editor Model: Recompute State */ + const initializePromise = this.initialize(); - this.updateBaseRangeAcceptedState(resultDiffs, states, tx); + this.onInitialized = this.onInitialized.then(async () => { + await initializePromise; + }); - if (shouldRecomputeHandledFromAccepted) { - shouldRecomputeHandledFromAccepted = false; - for (const [_range, observableState] of states) { - const state = observableState.accepted.get(); - observableState.handled.set(!(state.isEmpty || state.conflicting), tx); + initializePromise.then(() => { + let shouldRecomputeHandledFromAccepted = true; + this._register( + autorunHandleChanges( + 'Merge Editor Model: Recompute State From Result', + { + handleChange: (ctx) => { + if (ctx.didChange(this.modifiedBaseRangeResultStates)) { + shouldRecomputeHandledFromAccepted = true; } + return ctx.didChange(this.resultTextModelDiffs.diffs) + // Ignore non-text changes as we update the state directly + ? ctx.change === TextModelDiffChangeReason.textChange + : true; + }, + }, + (reader) => { + const states = this.modifiedBaseRangeResultStates.read(reader); + if (!this.isUpToDate.read(reader)) { + return; } - }); - } - ) - ); + const resultDiffs = this.resultTextModelDiffs.diffs.read(reader); + transaction(tx => { + /** @description Merge Editor Model: Recompute State */ - if (options.resetUnknownOnInitialization) { - this.onInitialized = this.onInitialized.then(() => { - this.resetDirtyConflictsToBase(); - }); + this.updateBaseRangeAcceptedState(resultDiffs, states, tx); + + if (shouldRecomputeHandledFromAccepted) { + shouldRecomputeHandledFromAccepted = false; + for (const [_range, observableState] of states) { + const state = observableState.accepted.get(); + observableState.handled.set(!(state.isEmpty || state.conflicting), tx); + } + } + }); + } + ) + ); + }); + } + + private async initialize(): Promise { + if (this.options.resetResult) { + await this.reset(); } } + public async reset(): Promise { + await waitForState(this.inputDiffComputingState, state => state === MergeEditorModelState.upToDate); + const states = this.modifiedBaseRangeResultStates.get(); + + transaction(tx => { + /** @description Set initial state */ + + for (const [range, state] of states) { + let newState: ModifiedBaseRangeState; + let handled = false; + if (range.input1Diffs.length === 0) { + newState = ModifiedBaseRangeState.default.withInput2(true); + handled = true; + } else if (range.input2Diffs.length === 0) { + newState = ModifiedBaseRangeState.default.withInput1(true); + handled = true; + } else { + newState = ModifiedBaseRangeState.default; + handled = false; + } + + state.accepted.set(newState, tx); + state.handled.set(handled, tx); + } + + this.resultTextModel.setValue(this.computeAutoMergedResult()); + }); + } + + private computeAutoMergedResult(): string { + const baseRanges = this.modifiedBaseRanges.get(); + + const baseLines = this.base.getLinesContent(); + const input1Lines = this.input1.textModel.getLinesContent(); + const input2Lines = this.input2.textModel.getLinesContent(); + + const resultLines: string[] = []; + function appendLinesToResult(source: string[], lineRange: LineRange) { + for (let i = lineRange.startLineNumber; i < lineRange.endLineNumberExclusive; i++) { + resultLines.push(source[i - 1]); + } + } + + let baseStartLineNumber = 1; + + for (const baseRange of baseRanges) { + appendLinesToResult(baseLines, LineRange.fromLineNumbers(baseStartLineNumber, baseRange.baseRange.startLineNumber)); + baseStartLineNumber = baseRange.baseRange.endLineNumberExclusive; + + if (baseRange.input1Diffs.length === 0) { + appendLinesToResult(input2Lines, baseRange.input2Range); + } else if (baseRange.input2Diffs.length === 0) { + appendLinesToResult(input1Lines, baseRange.input1Range); + } else { + appendLinesToResult(baseLines, baseRange.baseRange); + } + } + + appendLinesToResult(baseLines, LineRange.fromLineNumbers(baseStartLineNumber, baseLines.length + 1)); + + return resultLines.join('\n'); + } + public hasBaseRange(baseRange: ModifiedBaseRange): boolean { return this.modifiedBaseRangeResultStates.get().has(baseRange); } @@ -222,6 +293,21 @@ export class MergeEditorModel extends EditorModel { return MergeEditorModelState.upToDate; }); + public readonly inputDiffComputingState = derived('inputDiffComputingState', reader => { + const states = [ + this.input1TextModelDiffs, + this.input2TextModelDiffs, + ].map((s) => s.state.read(reader)); + + if (states.some((s) => s === TextModelDiffState.initializing)) { + return MergeEditorModelState.initializing; + } + if (states.some((s) => s === TextModelDiffState.updating)) { + return MergeEditorModelState.updating; + } + return MergeEditorModelState.upToDate; + }); + public readonly isUpToDate = derived('isUpToDate', reader => this.diffComputingState.read(reader) === MergeEditorModelState.upToDate); public readonly onInitialized = waitForState(this.diffComputingState, state => state === MergeEditorModelState.upToDate).then(() => { }); @@ -374,8 +460,8 @@ export class MergeEditorModel extends EditorModel { } public async resetResultToBaseAndAutoMerge() { + await waitForState(this.inputDiffComputingState, state => state === MergeEditorModelState.upToDate); this.resultTextModel.setValue(this.base.getValue()); - await waitForState(this.diffComputingState, state => state === MergeEditorModelState.upToDate); this.acceptNonConflictingDiffs(); } @@ -384,7 +470,12 @@ export class MergeEditorModel extends EditorModel { } public setHandled(baseRange: ModifiedBaseRange, handled: boolean, tx: ITransaction): void { - this.modifiedBaseRangeResultStates.get().get(baseRange)!.handled.set(handled, tx); + const state = this.modifiedBaseRangeResultStates.get().get(baseRange)!; + if (state.handled.get() === handled) { + return; + } + + state.handled.set(handled, tx); } public readonly unhandledConflictsCount = derived('unhandledConflictsCount', reader => { @@ -419,6 +510,54 @@ export class MergeEditorModel extends EditorModel { } return chunks.join(); } + + public async getResultValueWithConflictMarkers(): Promise { + await waitForState(this.diffComputingState, state => state === MergeEditorModelState.upToDate); + + if (this.unhandledConflictsCount.get() === 0) { + return this.resultTextModel.getValue(); + } + + const resultLines = this.resultTextModel.getLinesContent(); + const input1Lines = this.input1.textModel.getLinesContent(); + const input2Lines = this.input2.textModel.getLinesContent(); + + const states = this.modifiedBaseRangeResultStates.get(); + + const outputLines: string[] = []; + function appendLinesToResult(source: string[], lineRange: LineRange) { + for (let i = lineRange.startLineNumber; i < lineRange.endLineNumberExclusive; i++) { + outputLines.push(source[i - 1]); + } + } + + let resultStartLineNumber = 1; + + for (const [range, state] of states) { + if (state.handled.get()) { + continue; + } + const resultRange = this.resultTextModelDiffs.getResultLineRange(range.baseRange); + + appendLinesToResult(resultLines, LineRange.fromLineNumbers(resultStartLineNumber, Math.max(resultStartLineNumber, resultRange.startLineNumber))); + resultStartLineNumber = resultRange.endLineNumberExclusive; + + outputLines.push('<<<<<<<'); + if (state.accepted.get().conflicting) { + // to prevent loss of data, use modified result as "ours" + appendLinesToResult(resultLines, resultRange); + } else { + appendLinesToResult(input1Lines, range.input1Range); + } + outputLines.push('======='); + appendLinesToResult(input2Lines, range.input2Range); + outputLines.push('>>>>>>>'); + } + + appendLinesToResult(resultLines, LineRange.fromLineNumbers(resultStartLineNumber, resultLines.length + 1)); + return outputLines.join('\n'); + } + } interface ModifiedBaseRangeData { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange.ts index 9d901e00ba7..4a8e587260f 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange.ts @@ -244,6 +244,11 @@ export class ModifiedBaseRangeState { } } + public isInputIncluded(inputNumber: 1 | 2): boolean { + const value = this.getInput(inputNumber); + return value === InputState.first || value === InputState.second; + } + public withInputValue(inputNumber: 1 | 2, value: boolean): ModifiedBaseRangeState { return inputNumber === 1 ? this.withInput1(value) : this.withInput2(value); } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/utils.ts b/src/vs/workbench/contrib/mergeEditor/browser/utils.ts index 7e92f970387..30b984cda33 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/utils.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/utils.ts @@ -4,11 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { CompareResult, ArrayQueue } from 'vs/base/common/arrays'; -import { BugIndicatingError } from 'vs/base/common/errors'; +import { BugIndicatingError, onUnexpectedError } from 'vs/base/common/errors'; import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IObservable, autorun } from 'vs/base/common/observable'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { IModelDeltaDecoration } from 'vs/editor/common/model'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; export class ReentrancyBarrier { private isActive = false; @@ -158,3 +159,40 @@ export function deepMerge(source1: T, source2: Partial): T { } return result; } + +export class PersistentStore { + private hasValue = false; + private value: Readonly | undefined = undefined; + + constructor( + private readonly key: string, + @IStorageService private readonly storageService: IStorageService + ) { } + + public get(): Readonly | undefined { + if (!this.hasValue) { + const value = this.storageService.get(this.key, StorageScope.PROFILE); + if (value !== undefined) { + try { + this.value = JSON.parse(value) as any; + } catch (e) { + onUnexpectedError(e); + } + } + this.hasValue = true; + } + + return this.value; + } + + public set(newValue: T | undefined): void { + this.value = newValue; + + this.storageService.store( + this.key, + JSON.stringify(this.value), + StorageScope.PROFILE, + StorageTarget.USER + ); + } +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts index 56917808bce..c32608f9153 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { h } from 'vs/base/browser/dom'; +import { h, reset } from 'vs/base/browser/dom'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { autorun, IReader, observableFromEvent, observableSignal, observableSignalFromEvent, transaction } from 'vs/base/common/observable'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; @@ -52,6 +52,12 @@ export class EditorGutter extends D this._register(autorun('EditorGutter.Render', (reader) => this.render(reader))); } + override dispose(): void { + super.dispose(); + + reset(this._domNode); + } + private readonly views = new Map(); private render(reader: IReader): void { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/baseCodeEditorView.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/baseCodeEditorView.ts index 3c4131b5896..b69a2d49b26 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/baseCodeEditorView.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/baseCodeEditorView.ts @@ -5,15 +5,18 @@ import { reset } from 'vs/base/browser/dom'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; -import { autorun, derived, IObservable } from 'vs/base/common/observable'; +import { BugIndicatingError } from 'vs/base/common/errors'; +import { autorun, autorunWithStore, derived, IObservable } from 'vs/base/common/observable'; import { EditorExtensionsRegistry, IEditorContributionDescription } from 'vs/editor/browser/editorExtensions'; import { IModelDeltaDecoration, MinimapPosition, OverviewRulerLane } from 'vs/editor/common/model'; import { CodeLensContribution } from 'vs/editor/contrib/codelens/browser/codelensController'; import { localize } from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { applyObservableDecorations } from 'vs/workbench/contrib/mergeEditor/browser/utils'; import { handledConflictMinimapOverViewRulerColor, unhandledConflictMinimapOverViewRulerColor } from 'vs/workbench/contrib/mergeEditor/browser/view/colors'; +import { EditorGutter } from 'vs/workbench/contrib/mergeEditor/browser/view/editorGutter'; import { MergeEditorViewModel } from 'vs/workbench/contrib/mergeEditor/browser/view/viewModel'; import { CodeEditorView, createSelectionsAutorun, TitleMenu } from './codeEditorView'; @@ -21,8 +24,9 @@ export class BaseCodeEditorView extends CodeEditorView { constructor( viewModel: IObservable, @IInstantiationService instantiationService: IInstantiationService, + @IConfigurationService configurationService: IConfigurationService, ) { - super(instantiationService, viewModel); + super(instantiationService, viewModel, configurationService); this._register( createSelectionsAutorun(this, (baseRange, viewModel) => baseRange) @@ -32,6 +36,17 @@ export class BaseCodeEditorView extends CodeEditorView { instantiationService.createInstance(TitleMenu, MenuId.MergeBaseToolbar, this.htmlElements.title) ); + this._register( + autorunWithStore((reader, store) => { + if (this.checkboxesVisible.read(reader)) { + store.add(new EditorGutter(this.editor, this.htmlElements.gutterDiv, { + getIntersectingGutterItems: (range, reader) => [], + createView: (item, target) => { throw new BugIndicatingError(); }, + })); + } + }, 'update checkboxes') + ); + this._register( autorun('update labels & text model', (reader) => { const vm = this.viewModel.read(reader); @@ -54,6 +69,7 @@ export class BaseCodeEditorView extends CodeEditorView { const model = viewModel.model; const activeModifiedBaseRange = viewModel.activeModifiedBaseRange.read(reader); + const showNonConflictingChanges = viewModel.showNonConflictingChanges.read(reader); const result: IModelDeltaDecoration[] = []; for (const modifiedBaseRange of model.modifiedBaseRanges.read(reader)) { @@ -63,8 +79,12 @@ export class BaseCodeEditorView extends CodeEditorView { continue; } - const blockClassNames = ['merge-editor-block']; const isHandled = model.isHandled(modifiedBaseRange).read(reader); + if (!modifiedBaseRange.isConflicting && isHandled && !showNonConflictingChanges) { + continue; + } + + const blockClassNames = ['merge-editor-block']; if (isHandled) { blockClassNames.push('handled'); } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts index c53ac2ea2fc..27b27b276fc 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts @@ -15,6 +15,7 @@ import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { MenuId } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { DEFAULT_EDITOR_MAX_DIMENSIONS, DEFAULT_EDITOR_MIN_DIMENSIONS } from 'vs/workbench/browser/parts/editor/editor'; import { setStyle } from 'vs/workbench/contrib/mergeEditor/browser/utils'; @@ -58,6 +59,16 @@ export abstract class CodeEditorView extends Disposable { // snap?: boolean | undefined; }; + protected readonly checkboxesVisible = observableFromEvent( + this.configurationService.onDidChangeConfiguration, + () => /** @description checkboxesVisible */ this.configurationService.getValue('mergeEditor.showCheckboxes') ?? true + ); + + protected readonly codeLensesVisible = observableFromEvent( + this.configurationService.onDidChangeConfiguration, + () => /** @description codeLensesVisible */ this.configurationService.getValue('mergeEditor.showCodeLenses') ?? false + ); + public readonly editor = this.instantiationService.createInstance( CodeEditorWidget, this.htmlElements.editor, @@ -89,11 +100,12 @@ export abstract class CodeEditorView extends Disposable { public readonly cursorLineNumber = this.cursorPosition.map(p => /** @description cursorPosition.lineNumber */ p?.lineNumber); constructor( - @IInstantiationService private readonly instantiationService: IInstantiationService, public readonly viewModel: IObservable, + private readonly configurationService: IConfigurationService, ) { super(); + } protected getEditorContributions(): IEditorContributionDescription[] | undefined { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts index 27df6018ed1..9ad16bf2105 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts @@ -8,16 +8,22 @@ import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { Toggle } from 'vs/base/browser/ui/toggle/toggle'; import { Action, IAction, Separator } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; +import { Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { clamp } from 'vs/base/common/numbers'; -import { autorun, derived, IObservable, ISettableObservable, ITransaction, observableValue, transaction } from 'vs/base/common/observable'; +import { autorun, autorunWithStore, derived, IObservable, ISettableObservable, ITransaction, observableValue, transaction } from 'vs/base/common/observable'; import { noBreakWhitespace } from 'vs/base/common/strings'; import { isDefined } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; import { EditorExtensionsRegistry, IEditorContributionDescription } from 'vs/editor/browser/editorExtensions'; +import { CodeLens, CodeLensProvider, Command } from 'vs/editor/common/languages'; import { IModelDeltaDecoration, MinimapPosition, OverviewRulerLane } from 'vs/editor/common/model'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { CodeLensContribution } from 'vs/editor/contrib/codelens/browser/codelensController'; import { localize } from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { attachToggleStyler } from 'vs/platform/theme/common/styler'; @@ -30,24 +36,41 @@ import { EditorGutter, IGutterItemInfo, IGutterItemView } from '../editorGutter' import { CodeEditorView, createSelectionsAutorun, TitleMenu } from './codeEditorView'; export class InputCodeEditorView extends CodeEditorView { + public readonly otherInputNumber = this.inputNumber === 1 ? 2 : 1; + constructor( public readonly inputNumber: 1 | 2, viewModel: IObservable, @IInstantiationService instantiationService: IInstantiationService, @IContextMenuService contextMenuService: IContextMenuService, @IThemeService themeService: IThemeService, + @IConfigurationService configurationService: IConfigurationService, ) { - super(instantiationService, viewModel); + super(instantiationService, viewModel, configurationService); this.htmlElements.root.classList.add(`input`); this._register( - new EditorGutter(this.editor, this.htmlElements.gutterDiv, { - getIntersectingGutterItems: (range, reader) => { - return this.modifiedBaseRangeGutterItemInfos.read(reader); - }, - createView: (item, target) => new MergeConflictGutterItemView(item, target, contextMenuService, themeService), - }) + autorunWithStore((reader, store) => { + if (this.checkboxesVisible.read(reader)) { + store.add( + new EditorGutter(this.editor, this.htmlElements.gutterDiv, { + getIntersectingGutterItems: (range, reader) => { + return this.modifiedBaseRangeGutterItemInfos.read(reader); + }, + createView: (item, target) => new MergeConflictGutterItemView(item, target, contextMenuService, themeService), + }) + ); + } + }, 'update checkboxes') + ); + + this._register( + autorunWithStore((reader, store) => { + if (this.codeLensesVisible.read(reader)) { + store.add(instantiationService.createInstance(CodeLensPart, this)); + } + }, 'update code lens part') ); this._register( @@ -99,8 +122,10 @@ export class InputCodeEditorView extends CodeEditorView { const model = viewModel.model; const inputNumber = this.inputNumber; + const showNonConflictingChanges = viewModel.showNonConflictingChanges.read(reader); + return model.modifiedBaseRanges.read(reader) - .filter((r) => r.getInputDiffs(this.inputNumber).length > 0) + .filter((r) => r.getInputDiffs(this.inputNumber).length > 0 && (showNonConflictingChanges || r.isConflicting || !model.isHandled(r).read(reader))) .map((baseRange, idx) => new ModifiedBaseRangeGutterItemModel(idx.toString(), baseRange, inputNumber, viewModel)); }); @@ -115,6 +140,8 @@ export class InputCodeEditorView extends CodeEditorView { const result = new Array(); + const showNonConflictingChanges = viewModel.showNonConflictingChanges.read(reader); + for (const modifiedBaseRange of model.modifiedBaseRanges.read(reader)) { const range = modifiedBaseRange.getInputRange(this.inputNumber); if (!range) { @@ -135,6 +162,10 @@ export class InputCodeEditorView extends CodeEditorView { const inputClassName = this.inputNumber === 1 ? 'input1' : 'input2'; blockClassNames.push(inputClassName); + if (!modifiedBaseRange.isConflicting && !showNonConflictingChanges && isHandled) { + continue; + } + result.push({ range: range.toInclusiveRangeOrEmpty(), options: { @@ -185,10 +216,143 @@ export class InputCodeEditorView extends CodeEditorView { }); protected override getEditorContributions(): IEditorContributionDescription[] | undefined { + if (this.codeLensesVisible.get()) { + return undefined; + } return EditorExtensionsRegistry.getEditorContributions().filter(c => c.id !== CodeLensContribution.ID); } } +class CodeLensPart extends Disposable { + public static commandCounter = 0; + + constructor( + inputCodeEditorView: InputCodeEditorView, + @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, + ) { + super(); + + const codeLensCommandId = `mergeEditor.codeLensCommandInput${CodeLensPart.commandCounter++}`; + this._register(CommandsRegistry.registerCommand(codeLensCommandId, (accessor, arg) => { + arg(); + })); + + function command(title: string, callback: () => Promise): Command { + return { + title, + id: codeLensCommandId, + arguments: [callback], + }; + } + + const codeLenses = derived<{ codeLenses: CodeLens[]; uri: URI } | undefined>('codeLenses', reader => { + const viewModel = inputCodeEditorView.viewModel.read(reader); + if (!viewModel) { + return undefined; + } + const model = viewModel.model; + const inputData = inputCodeEditorView.inputNumber === 1 ? viewModel.model.input1 : viewModel.model.input2; + + const showNonConflictingChanges = viewModel.showNonConflictingChanges.read(reader); + + return { + codeLenses: viewModel.model.modifiedBaseRanges.read(reader).flatMap(r => { + const range = r.getInputRange(inputCodeEditorView.inputNumber).toRange(); + + const handled = model.isHandled(r).read(reader); + const state = model.getState(r).read(reader); + const result: CodeLens[] = []; + + if (!r.isConflicting && handled && !showNonConflictingChanges) { + return []; + } + + if (!state.conflicting && !state.isInputIncluded(inputCodeEditorView.inputNumber)) { + result.push( + { + range, + command: + !state.isInputIncluded(inputCodeEditorView.inputNumber) + ? command(`$(pass) Accept ${inputData.title}`, async () => { + transaction((tx) => { + model.setState( + r, + state.withInputValue(inputCodeEditorView.inputNumber, true), + true, + tx + ); + }); + }) + : command(`$(error) Remove ${inputData.title}`, async () => { + transaction((tx) => { + model.setState( + r, + state.withInputValue(inputCodeEditorView.inputNumber, false), + true, + tx + ); + }); + }), + } + ); + + if (r.canBeCombined && state.isEmpty) { + result.push({ + range, + command: + state.input1 && state.input2 + ? command(`$(error) Remove Both`, async () => { + transaction((tx) => { + model.setState( + r, + ModifiedBaseRangeState.default, + true, + tx + ); + }); + }) + : command(`$(pass) Accept Both`, async () => { + transaction((tx) => { + model.setState( + r, + state + .withInputValue(inputCodeEditorView.inputNumber, true) + .withInputValue(inputCodeEditorView.otherInputNumber, true), + true, + tx + ); + }); + }), + }); + } + } + if (result.length === 0) { + result.push({ + range: range, + command: command(` `, async () => { }) + }); + } + return result; + }), + uri: inputData.textModel.uri + }; + }); + + const codeLensProvider: CodeLensProvider = { + onDidChange: Event.map(Event.fromObservable(codeLenses), () => codeLensProvider), + async provideCodeLenses(model, token) { + const result = codeLenses.get(); + if (!result || result.uri.toString() !== model.uri.toString()) { + return { lenses: [], dispose: () => { } }; + } + return { lenses: result.codeLenses, dispose: () => { } }; + } + }; + + this._register(languageFeaturesService.codeLensProvider.register({ pattern: '**/*' }, codeLensProvider)); + } +} + export class ModifiedBaseRangeGutterItemModel implements IGutterItemInfo { private readonly model = this.viewModel.model; public readonly range = this.baseRange.getInputRange(this.inputNumber); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/resultCodeEditorView.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/resultCodeEditorView.ts index dfa4da56871..82c2ff6d315 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/resultCodeEditorView.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/resultCodeEditorView.ts @@ -7,16 +7,23 @@ import { reset } from 'vs/base/browser/dom'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { CompareResult } from 'vs/base/common/arrays'; import { BugIndicatingError } from 'vs/base/common/errors'; -import { toDisposable } from 'vs/base/common/lifecycle'; -import { autorun, derived, IObservable } from 'vs/base/common/observable'; +import { Event } from 'vs/base/common/event'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { autorun, autorunWithStore, derived, IObservable, transaction } from 'vs/base/common/observable'; +import { URI } from 'vs/base/common/uri'; +import { CodeLens, CodeLensProvider, Command } from 'vs/editor/common/languages'; import { IModelDeltaDecoration, MinimapPosition, OverviewRulerLane } from 'vs/editor/common/model'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { localize } from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { MergeMarkersController } from 'vs/workbench/contrib/mergeEditor/browser/mergeMarkers/mergeMarkersController'; import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange'; +import { ModifiedBaseRangeState } from 'vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange'; import { applyObservableDecorations, join } from 'vs/workbench/contrib/mergeEditor/browser/utils'; import { handledConflictMinimapOverViewRulerColor, unhandledConflictMinimapOverViewRulerColor } from 'vs/workbench/contrib/mergeEditor/browser/view/colors'; import { EditorGutter } from 'vs/workbench/contrib/mergeEditor/browser/view/editorGutter'; @@ -29,8 +36,9 @@ export class ResultCodeEditorView extends CodeEditorView { viewModel: IObservable, @IInstantiationService instantiationService: IInstantiationService, @ILabelService private readonly _labelService: ILabelService, + @IConfigurationService configurationService: IConfigurationService, ) { - super(instantiationService, viewModel); + super(instantiationService, viewModel, configurationService); this.editor.invokeWithinContext(accessor => { const contextKeyService = accessor.get(IContextKeyService); @@ -41,13 +49,25 @@ export class ResultCodeEditorView extends CodeEditorView { this._register(new MergeMarkersController(this.editor, this.viewModel)); + this._register( + autorunWithStore((reader, store) => { + if (this.codeLensesVisible.read(reader)) { + store.add(instantiationService.createInstance(CodeLensPart, this)); + } + }, 'update code lens part') + ); + this.htmlElements.gutterDiv.style.width = '5px'; this._register( - new EditorGutter(this.editor, this.htmlElements.gutterDiv, { - getIntersectingGutterItems: (range, reader) => [], - createView: (item, target) => { throw new BugIndicatingError(); }, - }) + autorunWithStore((reader, store) => { + if (this.checkboxesVisible.read(reader)) { + store.add(new EditorGutter(this.editor, this.htmlElements.gutterDiv, { + getIntersectingGutterItems: (range, reader) => [], + createView: (item, target) => { throw new BugIndicatingError(); }, + })); + } + }, 'update checkboxes') ); this._register(autorun('update labels & text model', reader => { @@ -125,6 +145,8 @@ export class ResultCodeEditorView extends CodeEditorView { const activeModifiedBaseRange = viewModel.activeModifiedBaseRange.read(reader); + const showNonConflictingChanges = viewModel.showNonConflictingChanges.read(reader); + for (const m of baseRangeWithStoreAndTouchingDiffs) { const modifiedBaseRange = m.left; @@ -142,6 +164,10 @@ export class ResultCodeEditorView extends CodeEditorView { } blockClassNames.push('result'); + if (!modifiedBaseRange.isConflicting && !showNonConflictingChanges && isHandled) { + continue; + } + result.push({ range: model.getLineRangeInResult(modifiedBaseRange.baseRange, reader).toInclusiveRangeOrEmpty(), options: { @@ -193,3 +219,147 @@ export class ResultCodeEditorView extends CodeEditorView { return result; }); } + +class CodeLensPart extends Disposable { + public static commandCounter = 0; + + constructor( + resultCodeEditorView: ResultCodeEditorView, + @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, + ) { + super(); + + const codeLensCommandId = `mergeEditor.codeLensCommandResult${CodeLensPart.commandCounter++}`; + this._register(CommandsRegistry.registerCommand(codeLensCommandId, (accessor, arg) => { + arg(); + })); + + function command(title: string, callback: () => Promise): Command { + return { + title, + id: codeLensCommandId, + arguments: [callback], + }; + } + + const codeLenses = derived<{ codeLenses: CodeLens[]; uri: URI } | undefined>('codeLenses', reader => { + const viewModel = resultCodeEditorView.viewModel.read(reader); + if (!viewModel) { + return undefined; + } + const model = viewModel.model; + const showNonConflictingChanges = viewModel.showNonConflictingChanges.read(reader); + + return { + codeLenses: viewModel.model.modifiedBaseRanges.read(reader).flatMap(r => { + const range = model.getLineRangeInResult(r.baseRange, reader).toRange(); + + const handled = model.isHandled(r).read(reader); + const state = model.getState(r).read(reader); + const result: CodeLens[] = []; + + if (!r.isConflicting && handled && !showNonConflictingChanges) { + return []; + } + + const stateLabel = ((state: ModifiedBaseRangeState): string => { + if (state.conflicting) { + return '= Manual Resolution'; + } else if (state.isEmpty) { + return '= Base'; + } else { + const labels = []; + if (state.input1) { + labels.push(model.input1.title); + } + if (state.input2) { + labels.push(model.input2.title); + } + return `= ${labels.join(' + ')}`; + } + })(state); + + result.push({ + range, + command: { + title: stateLabel, + id: 'notSupported', + } + }); + + + const stateToggles: CodeLens[] = []; + if (state.input1) { + result.push({ + range, + command: command(`$(error) Remove ${model.input1.title}`, async () => { + transaction((tx) => { + model.setState( + r, + state.withInputValue(1, false), + true, + tx + ); + }); + }), + }); + } + if (state.input2) { + result.push({ + range, + command: command(`$(error) Remove ${model.input2.title}`, async () => { + transaction((tx) => { + model.setState( + r, + state.withInputValue(2, false), + true, + tx + ); + }); + }), + }); + } + if (state.input2First) { + stateToggles.reverse(); + } + result.push(...stateToggles); + + + + if (state.conflicting) { + result.push( + { + range, + command: command(`$(error) Reset to base`, async () => { + transaction((tx) => { + model.setState( + r, + ModifiedBaseRangeState.default, + true, + tx + ); + }); + }) + } + ); + } + return result; + }), + uri: model.resultTextModel.uri, + }; + }); + + const codeLensProvider: CodeLensProvider = { + onDidChange: Event.map(Event.fromObservable(codeLenses), () => codeLensProvider), + async provideCodeLenses(model, token) { + const result = codeLenses.get(); + if (!result || result.uri.toString() !== model.uri.toString()) { + return { lenses: [], dispose: () => { } }; + } + return { lenses: result.codeLenses, dispose: () => { } }; + } + }; + + this._register(languageFeaturesService.codeLensProvider.register({ pattern: '**/*' }, codeLensProvider)); + } +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts index d9eb968533a..27d137a9be4 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts @@ -6,12 +6,13 @@ import { $, Dimension, reset } from 'vs/base/browser/dom'; import { Grid, GridNodeDescriptor, IView, SerializableGrid } from 'vs/base/browser/ui/grid/grid'; import { Orientation } from 'vs/base/browser/ui/splitview/splitview'; +import { CompareResult, lastOrDefault } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Color } from 'vs/base/common/color'; import { BugIndicatingError, onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { autorun, autorunWithStore, IObservable, IReader, observableValue } from 'vs/base/common/observable'; +import { autorun, autorunWithStore, IObservable, IReader, observableValue, transaction } from 'vs/base/common/observable'; import { basename, isEqual } from 'vs/base/common/resources'; import { isDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; @@ -36,12 +37,16 @@ import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { applyTextEditorOptions } from 'vs/workbench/common/editor/editorOptions'; import { readTransientState, writeTransientState } from 'vs/workbench/contrib/codeEditor/browser/toggleWordWrap'; import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; +import { IMergeEditorInputModel } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel'; +import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange'; +import { DetailedLineRangeMapping } from 'vs/workbench/contrib/mergeEditor/browser/model/mapping'; import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel'; -import { deepMerge, thenIfNotDisposed } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { ModifiedBaseRange } from 'vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange'; +import { deepMerge, join, PersistentStore, thenIfNotDisposed } from 'vs/workbench/contrib/mergeEditor/browser/utils'; import { BaseCodeEditorView } from 'vs/workbench/contrib/mergeEditor/browser/view/editors/baseCodeEditorView'; import { ScrollSynchronizer } from 'vs/workbench/contrib/mergeEditor/browser/view/scrollSynchronizer'; import { MergeEditorViewModel } from 'vs/workbench/contrib/mergeEditor/browser/view/viewModel'; -import { ctxIsMergeEditor, ctxMergeBaseUri, ctxMergeEditorLayout, ctxMergeEditorShowBase, ctxMergeResultUri, MergeEditorLayoutKind } from 'vs/workbench/contrib/mergeEditor/common/mergeEditor'; +import { ctxIsMergeEditor, ctxMergeBaseUri, ctxMergeEditorLayout, ctxMergeEditorShowBase, ctxMergeEditorShowNonConflictingChanges, ctxMergeResultUri, MergeEditorLayoutKind } from 'vs/workbench/contrib/mergeEditor/common/mergeEditor'; import { settingsSashBorder } from 'vs/workbench/contrib/preferences/common/settingsEditorColorRegistry'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorResolverService, MergeEditorInputFactoryFunction, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; @@ -70,13 +75,21 @@ export class MergeEditor extends AbstractTextEditor { private readonly input2View = this._register(this.instantiationService.createInstance(InputCodeEditorView, 2, this._viewModel)); private readonly inputResultView = this._register(this.instantiationService.createInstance(ResultCodeEditorView, this._viewModel)); - private readonly _layoutMode: MergeEditorLayoutStore; + private readonly _layoutMode = this.instantiationService.createInstance(MergeEditorLayoutStore); + private readonly _layoutModeObs = observableValue('layoutMode', this._layoutMode.value); private readonly _ctxIsMergeEditor: IContextKey; private readonly _ctxUsesColumnLayout: IContextKey; private readonly _ctxShowBase: IContextKey; private readonly _ctxResultUri: IContextKey; private readonly _ctxBaseUri: IContextKey; - public get model(): MergeEditorModel | undefined { return this._viewModel.get()?.model; } + private readonly _ctxShowNonConflictingChanges: IContextKey; + private readonly _inputModel = observableValue('inputModel', undefined); + public get inputModel(): IObservable { + return this._inputModel; + } + public get model(): MergeEditorModel | undefined { + return this.inputModel.get()?.model; + } private get inputsWritable(): boolean { return !!this._configurationService.getValue('mergeEditor.writableInputs'); @@ -102,8 +115,7 @@ export class MergeEditor extends AbstractTextEditor { this._ctxBaseUri = ctxMergeBaseUri.bindTo(contextKeyService); this._ctxResultUri = ctxMergeResultUri.bindTo(contextKeyService); this._ctxShowBase = ctxMergeEditorShowBase.bindTo(contextKeyService); - - this._layoutMode = instantiation.createInstance(MergeEditorLayoutStore); + this._ctxShowNonConflictingChanges = ctxMergeEditorShowNonConflictingChanges.bindTo(contextKeyService); this._register(new ScrollSynchronizer(this._viewModel, this.input1View, this.input2View, this.baseView, this.inputResultView)); } @@ -112,6 +124,7 @@ export class MergeEditor extends AbstractTextEditor { this._sessionDisposables.dispose(); this._ctxIsMergeEditor.reset(); this._ctxUsesColumnLayout.reset(); + this._ctxShowNonConflictingChanges.reset(); super.dispose(); } @@ -176,16 +189,23 @@ export class MergeEditor extends AbstractTextEditor { await super.setInput(input, options, context, token); this._sessionDisposables.clear(); - this._viewModel.set(undefined, undefined); + transaction(tx => { + this._viewModel.set(undefined, tx); + this._inputModel.set(undefined, tx); + }); - const model = await input.resolve(); + const inputModel = await input.resolve(); + const model = inputModel.model; - const viewModel = new MergeEditorViewModel(model, this.input1View, this.input2View, this.inputResultView, this.baseView); - this._viewModel.set(viewModel, undefined); + const viewModel = new MergeEditorViewModel(model, this.input1View, this.input2View, this.inputResultView, this.baseView, this.showNonConflictingChanges); + transaction(tx => { + this._viewModel.set(viewModel, tx); + this._inputModel.set(inputModel, tx); + }); this._sessionDisposables.add(viewModel); // Set/unset context keys based on input - this._ctxResultUri.set(model.resultTextModel.uri.toString()); + this._ctxResultUri.set(inputModel.resultUri.toString()); this._ctxBaseUri.set(model.base.uri.toString()); this._sessionDisposables.add(toDisposable(() => { this._ctxBaseUri.reset(); @@ -198,17 +218,35 @@ export class MergeEditor extends AbstractTextEditor { const input1ViewZoneIds: string[] = []; const input2ViewZoneIds: string[] = []; const baseViewZoneIds: string[] = []; + const resultViewZoneIds: string[] = []; const baseView = this.baseView.read(reader); - this.input1View.editor.changeViewZones(input1ViewZoneAccessor => { - this.input2View.editor.changeViewZones(input2ViewZoneAccessor => { - if (baseView) { - baseView.editor.changeViewZones(baseViewZoneAccessor => { - setViewZones(reader, input1ViewZoneIds, input1ViewZoneAccessor, input2ViewZoneIds, input2ViewZoneAccessor, baseViewZoneIds, baseViewZoneAccessor); - }); - } else { - setViewZones(reader, input1ViewZoneIds, input1ViewZoneAccessor, input2ViewZoneIds, input2ViewZoneAccessor, baseViewZoneIds, undefined); - } + this.inputResultView.editor.changeViewZones(resultViewZoneAccessor => { + const actualResultViewZoneAccessor = this._layoutModeObs.read(reader).kind === 'columns' ? resultViewZoneAccessor : undefined; + + this.input1View.editor.changeViewZones(input1ViewZoneAccessor => { + this.input2View.editor.changeViewZones(input2ViewZoneAccessor => { + if (baseView) { + baseView.editor.changeViewZones(baseViewZoneAccessor => { + setViewZones(reader, + input1ViewZoneIds, input1ViewZoneAccessor, + input2ViewZoneIds, input2ViewZoneAccessor, + baseViewZoneIds, /*baseViewZoneAccessor,*/ undefined, + resultViewZoneIds, actualResultViewZoneAccessor, + ); + }); + } else { + setViewZones(reader, + input1ViewZoneIds, + input1ViewZoneAccessor, + input2ViewZoneIds, + input2ViewZoneAccessor, + baseViewZoneIds, + undefined, + resultViewZoneIds, actualResultViewZoneAccessor, + ); + } + }); }); }); @@ -229,6 +267,11 @@ export class MergeEditor extends AbstractTextEditor { a.removeZone(zone); } }); + this.inputResultView.editor.changeViewZones(a => { + for (const zone of resultViewZoneIds) { + a.removeZone(zone); + } + }); } }); @@ -305,17 +348,67 @@ export class MergeEditor extends AbstractTextEditor { input2ViewZoneIds: string[], input2ViewZoneAccessor: IViewZoneChangeAccessor, baseViewZoneIds: string[], - baseViewZoneAccessor: IViewZoneChangeAccessor | undefined + baseViewZoneAccessor: IViewZoneChangeAccessor | undefined, + resultViewZoneIds: string[], + resultViewZoneAccessor: IViewZoneChangeAccessor | undefined, ) { let input1LinesAdded = 0; let input2LinesAdded = 0; let baseLinesAdded = 0; + let resultLinesAdded = 0; - for (const m of model.modifiedBaseRanges.read(reader)) { - const alignedLines: [number | undefined, number, number | undefined][] = - getAlignments(m); + const resultDiffs = model.baseResultDiffs.read(reader); + const baseRangeWithStoreAndTouchingDiffs = join( + model.modifiedBaseRanges.read(reader), + resultDiffs, + (baseRange, diff) => + baseRange.baseRange.touches(diff.inputRange) + ? CompareResult.neitherLessOrGreaterThan + : LineRange.compareByStart( + baseRange.baseRange, + diff.inputRange + ) + ); - for (const [input1Line, baseLine, input2Line] of alignedLines) { + let lastModifiedBaseRange: ModifiedBaseRange | undefined = undefined; + let lastBaseResultDiff: DetailedLineRangeMapping | undefined = undefined; + for (const m of baseRangeWithStoreAndTouchingDiffs) { + interface LineAlignment { + baseLine: number; + input1Line?: number; + input2Line?: number; + resultLine?: number; + } + + const lastResultDiff = lastOrDefault(m.rights)!; + if (lastResultDiff) { + lastBaseResultDiff = lastResultDiff; + } + let alignedLines: LineAlignment[]; + if (m.left) { + alignedLines = getAlignments(m.left).map(a => ({ + input1Line: a[0], + baseLine: a[1], + input2Line: a[2], + resultLine: undefined, + })); + + lastModifiedBaseRange = m.left; + // This is a total hack. + alignedLines[alignedLines.length - 1].resultLine = + m.left.baseRange.endLineNumberExclusive + + (lastBaseResultDiff ? lastBaseResultDiff.resultingDeltaFromOriginalToModified : 0); + + } else { + alignedLines = [{ + baseLine: lastResultDiff.inputRange.endLineNumberExclusive, + input1Line: lastResultDiff.inputRange.endLineNumberExclusive + (lastModifiedBaseRange ? (lastModifiedBaseRange.input1Range.endLineNumberExclusive - lastModifiedBaseRange.baseRange.endLineNumberExclusive) : 0), + input2Line: lastResultDiff.inputRange.endLineNumberExclusive + (lastModifiedBaseRange ? (lastModifiedBaseRange.input2Range.endLineNumberExclusive - lastModifiedBaseRange.baseRange.endLineNumberExclusive) : 0), + resultLine: lastResultDiff.outputRange.endLineNumberExclusive, + }]; + } + + for (const { input1Line, baseLine, input2Line, resultLine } of alignedLines) { if (!baseViewZoneAccessor && (input1Line === undefined || input2Line === undefined)) { continue; } @@ -325,8 +418,9 @@ export class MergeEditor extends AbstractTextEditor { const input2Line_ = input2Line !== undefined ? input2Line + input2LinesAdded : -1; const baseLine_ = baseLine + baseLinesAdded; + const resultLine_ = resultLine !== undefined ? resultLine + resultLinesAdded : -1; - const max = Math.max(baseViewZoneAccessor ? baseLine_ : 0, input1Line_, input2Line_); + const max = Math.max(baseViewZoneAccessor ? baseLine_ : 0, input1Line_, input2Line_, resultLine_); if (input1Line !== undefined) { const diffInput1 = max - input1Line_; @@ -369,6 +463,20 @@ export class MergeEditor extends AbstractTextEditor { baseLinesAdded += diffBase; } } + + if (resultViewZoneAccessor && resultLine !== undefined) { + const diffResult = max - resultLine_; + if (diffResult > 0) { + resultViewZoneIds.push( + resultViewZoneAccessor.addZone({ + afterLineNumber: resultLine - 1, + heightInLines: diffResult, + domNode: $('div.diagonal-fill'), + }) + ); + resultLinesAdded += diffResult; + } + } } } } @@ -457,58 +565,63 @@ export class MergeEditor extends AbstractTextEditor { private readonly baseViewDisposables = this._register(new DisposableStore()); private applyLayout(layout: IMergeEditorLayout): void { - if (layout.showBase && !this.baseView.get()) { - this.baseViewDisposables.clear(); - const baseView = this.baseViewDisposables.add( - this.instantiationService.createInstance( - BaseCodeEditorView, - this.viewModel - ) - ); - this.baseViewDisposables.add(autorun('Update base view options', reader => { - const options = this.baseViewOptions.read(reader); - if (options) { - baseView.updateOptions(options); - } - })); - this.baseView.set(baseView, undefined); - } else if (!layout.showBase && this.baseView.get()) { - this.baseView.set(undefined, undefined); - this.baseViewDisposables.clear(); - } + transaction(tx => { + /** @description applyLayout */ - if (layout.kind === 'mixed') { - this.setGrid([ - layout.showBase ? { - size: 38, - data: this.baseView.get()!.view - } : undefined, - { - size: 38, - groups: [{ data: this.input1View.view }, { data: this.input2View.view }] - }, - { - size: 62, - data: this.inputResultView.view - }, - ].filter(isDefined)); - } else if (layout.kind === 'columns') { - this.setGrid([ - layout.showBase ? { - size: 40, - data: this.baseView.get()!.view - } : undefined, - { - size: 60, - groups: [{ data: this.input1View.view }, { data: this.inputResultView.view }, { data: this.input2View.view }] - }, - ].filter(isDefined)); - } + if (layout.showBase && !this.baseView.get()) { + this.baseViewDisposables.clear(); + const baseView = this.baseViewDisposables.add( + this.instantiationService.createInstance( + BaseCodeEditorView, + this.viewModel + ) + ); + this.baseViewDisposables.add(autorun('Update base view options', reader => { + const options = this.baseViewOptions.read(reader); + if (options) { + baseView.updateOptions(options); + } + })); + this.baseView.set(baseView, tx); + } else if (!layout.showBase && this.baseView.get()) { + this.baseView.set(undefined, tx); + this.baseViewDisposables.clear(); + } - this._layoutMode.value = layout; - this._ctxUsesColumnLayout.set(layout.kind); - this._ctxShowBase.set(layout.showBase); - this._onDidChangeSizeConstraints.fire(); + if (layout.kind === 'mixed') { + this.setGrid([ + layout.showBase ? { + size: 38, + data: this.baseView.get()!.view + } : undefined, + { + size: 38, + groups: [{ data: this.input1View.view }, { data: this.input2View.view }] + }, + { + size: 62, + data: this.inputResultView.view + }, + ].filter(isDefined)); + } else if (layout.kind === 'columns') { + this.setGrid([ + layout.showBase ? { + size: 40, + data: this.baseView.get()!.view + } : undefined, + { + size: 60, + groups: [{ data: this.input1View.view }, { data: this.inputResultView.view }, { data: this.input2View.view }] + }, + ].filter(isDefined)); + } + + this._layoutMode.value = layout; + this._ctxUsesColumnLayout.set(layout.kind); + this._ctxShowBase.set(layout.showBase); + this._onDidChangeSizeConstraints.fire(); + this._layoutModeObs.set(layout, tx); + }); } private setGrid(descriptor: GridNodeDescriptor[]) { @@ -552,7 +665,7 @@ export class MergeEditor extends AbstractTextEditor { } protected computeEditorViewState(resource: URI): IMergeEditorViewState | undefined { - if (!isEqual(this.model?.resultTextModel.uri, resource)) { + if (!isEqual(this.inputModel.get()?.resultUri, resource)) { return undefined; } const result = this.inputResultView.editor.saveViewState(); @@ -569,6 +682,15 @@ export class MergeEditor extends AbstractTextEditor { protected tracksEditorViewState(input: EditorInput): boolean { return input instanceof MergeEditorInput; } + + private readonly showNonConflictingChangesStore = this.instantiationService.createInstance(PersistentStore, 'mergeEditor/showNonConflictingChanges'); + private readonly showNonConflictingChanges = observableValue('showNonConflictingChanges', this.showNonConflictingChangesStore.get() ?? false); + + public toggleShowNonConflictingChanges(): void { + this.showNonConflictingChanges.set(!this.showNonConflictingChanges.get(), undefined); + this.showNonConflictingChangesStore.set(this.showNonConflictingChanges.get()); + this._ctxShowNonConflictingChanges.set(this.showNonConflictingChanges.get()); + } } interface IMergeEditorLayout { @@ -576,6 +698,7 @@ interface IMergeEditorLayout { readonly showBase: boolean; } +// TODO use PersistentStore class MergeEditorLayoutStore { private static readonly _key = 'mergeEditor/layout'; private _value: IMergeEditorLayout = { kind: 'mixed', showBase: false }; diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts index b73abe69d23..d16dc21b25d 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts @@ -27,6 +27,7 @@ export class MergeEditorViewModel extends Disposable { public readonly inputCodeEditorView2: InputCodeEditorView, public readonly resultCodeEditorView: ResultCodeEditorView, public readonly baseCodeEditorView: IObservable, + public readonly showNonConflictingChanges: IObservable, ) { super(); diff --git a/src/vs/workbench/contrib/mergeEditor/common/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/common/mergeEditor.ts index 326aec3eddd..f5c4b9f7dcf 100644 --- a/src/vs/workbench/contrib/mergeEditor/common/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/common/mergeEditor.ts @@ -12,6 +12,7 @@ export const ctxIsMergeEditor = new RawContextKey('isMergeEditor', fals export const ctxIsMergeResultEditor = new RawContextKey('isMergeResultEditor', false, { type: 'boolean', description: localize('isr', 'The editor is a the result editor of a merge editor.') }); export const ctxMergeEditorLayout = new RawContextKey('mergeEditorLayout', 'mixed', { type: 'string', description: localize('editorLayout', 'The layout mode of a merge editor') }); export const ctxMergeEditorShowBase = new RawContextKey('mergeEditorShowBase', false, { type: 'boolean', description: localize('showBase', 'If the merge editor shows the base version') }); +export const ctxMergeEditorShowNonConflictingChanges = new RawContextKey('mergeEditorShowNonConflictingChanges', false, { type: 'boolean', description: localize('showNonConflictingChanges', 'If the merge editor shows non-conflicting changes') }); export const ctxMergeBaseUri = new RawContextKey('mergeEditorBaseUri', '', { type: 'string', description: localize('baseUri', 'The uri of the baser of a merge editor') }); export const ctxMergeResultUri = new RawContextKey('mergeEditorResultUri', '', { type: 'string', description: localize('resultUri', 'The uri of the result of a merge editor') }); diff --git a/src/vs/workbench/contrib/mergeEditor/test/browser/model.test.ts b/src/vs/workbench/contrib/mergeEditor/test/browser/model.test.ts index 33a3613fbe6..1d1c0f84a08 100644 --- a/src/vs/workbench/contrib/mergeEditor/test/browser/model.test.ts +++ b/src/vs/workbench/contrib/mergeEditor/test/browser/model.test.ts @@ -293,7 +293,9 @@ class MergeModelInterface extends Disposable { resultTextModel, diffComputer, diffComputer, - { resetUnknownOnInitialization: false } + { + resetResult: false + } )); } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts index 8acf11d042a..c3fba768976 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { disposableTimeout, RunOnceScheduler } from 'vs/base/common/async'; -import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, dispose, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { themeColorFromId, ThemeIcon } from 'vs/platform/theme/common/themeService'; @@ -55,14 +55,14 @@ export class NotebookStatusBarController extends Disposable { return; } - for (const newCell of e.added) { - this._visibleCells.set(newCell.handle, this._itemFactory(vm, newCell)); - } - for (const oldCell of e.removed) { this._visibleCells.get(oldCell.handle)?.dispose(); this._visibleCells.delete(oldCell.handle); } + + for (const newCell of e.added) { + this._visibleCells.set(newCell.handle, this._itemFactory(vm, newCell)); + } } override dispose(): void { @@ -94,7 +94,7 @@ class ExecutionStateCellStatusBarItem extends Disposable { private _currentItemIds: string[] = []; private _showedExecutingStateTime: number | undefined; - private _clearExecutingStateTimer: IDisposable | undefined; + private _clearExecutingStateTimer = this._register(new MutableDisposable()); constructor( private readonly _notebookViewModel: INotebookViewModel, @@ -130,10 +130,10 @@ class ExecutionStateCellStatusBarItem extends Disposable { } else if (runState?.state !== NotebookCellExecutionState.Executing && typeof this._showedExecutingStateTime === 'number') { const timeUntilMin = ExecutionStateCellStatusBarItem.MIN_SPINNER_TIME - (Date.now() - this._showedExecutingStateTime); if (timeUntilMin > 0) { - if (!this._clearExecutingStateTimer) { - this._clearExecutingStateTimer = disposableTimeout(() => { + if (!this._clearExecutingStateTimer.value) { + this._clearExecutingStateTimer.value = disposableTimeout(() => { this._showedExecutingStateTime = undefined; - this._clearExecutingStateTimer = undefined; + this._clearExecutingStateTimer.clear(); this._update(); }, timeUntilMin); } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts b/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts index ca7dea6f626..ad13e9be7a0 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts @@ -142,7 +142,7 @@ registerAction2(class extends Action2 { } const notebook = editor.textModel; - const { selected, all, suggestions } = notebookKernelService.getMatchingKernel(notebook); + const { selected, all, suggestions, hidden } = notebookKernelService.getMatchingKernel(notebook); if (selected && controllerId && selected.id === controllerId && ExtensionIdentifier.equals(selected.extension, extensionId)) { // current kernel is wanted kernel -> done @@ -200,9 +200,9 @@ registerAction2(class extends Action2 { quickPickItems.push(...suggestions.map(toQuickPick)); } - // Next display all of the kernels grouped by categories or extensions. + // Next display all of the kernels not marked as hidden grouped by categories or extensions. // If we don't have a kind, always display those at the bottom. - const picks = all.filter(item => !suggestions.includes(item)).map(toQuickPick); + const picks = all.filter(item => (!suggestions.includes(item) && !hidden.includes(item))).map(toQuickPick); const kernelsPerCategory = groupBy(picks, (a, b) => compareIgnoreCase(a.kernel.kind || 'z', b.kernel.kind || 'z')); kernelsPerCategory.forEach(items => { quickPickItems.push({ @@ -463,7 +463,8 @@ export class KernelStatus extends Disposable implements IWorkbenchContribution { this._kernelInfoElement.clear(); const { selected, suggestions, all } = this._notebookKernelService.getMatchingKernel(notebook); - const suggested = (suggestions.length === 1 && all.length === 1) ? suggestions[0] : undefined; + const suggested = (suggestions.length === 1 ? suggestions[0] : undefined) + ?? (all.length === 1) ? all[0] : undefined; let isSuggested = false; if (all.length === 0) { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts b/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts index ab714f85b51..3644400d74b 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts @@ -70,7 +70,7 @@ export class TroubleshootController extends Disposable implements INotebookEdito } this._localStore.add(this._notebookEditor.onDidChangeViewCells(e => { - e.splices.reverse().forEach(splice => { + [...e.splices].reverse().forEach(splice => { const [start, deleted, newCells] = splice; const deletedCells = this._cellStateListeners.splice(start, deleted, ...newCells.map(cell => { return cell.onDidChangeLayout((e: ICommonCellViewModelLayoutChangeInfo) => { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 5a3028c7400..b95a790cc6d 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -58,8 +58,8 @@ KERNEL_RECOMMENDATIONS.get(IPYNB_VIEW_TYPE)?.set('python', { }); export interface INotebookExtensionRecommendation { - extensionId: string; - displayName?: string; + readonly extensionId: string; + readonly displayName?: string; } //#endregion @@ -77,16 +77,16 @@ export const enum RenderOutputType { } export interface IRenderPlainHtmlOutput { - type: RenderOutputType.Html; - source: IDisplayOutputViewModel; - htmlContent: string; + readonly type: RenderOutputType.Html; + readonly source: IDisplayOutputViewModel; + readonly htmlContent: string; } export interface IRenderOutputViaExtension { - type: RenderOutputType.Extension; - source: IDisplayOutputViewModel; - mimeType: string; - renderer: INotebookRendererInfo; + readonly type: RenderOutputType.Extension; + readonly source: IDisplayOutputViewModel; + readonly mimeType: string; + readonly renderer: INotebookRendererInfo; } export type IInsetRenderOutput = IRenderPlainHtmlOutput | IRenderOutputViaExtension; @@ -135,9 +135,9 @@ export interface IDisplayOutputLayoutUpdateRequest { } export interface ICommonCellInfo { - cellId: string; - cellHandle: number; - cellUri: URI; + readonly cellId: string; + readonly cellHandle: number; + readonly cellUri: URI; } export interface IFocusNotebookCellOptions { @@ -173,14 +173,14 @@ export interface CodeCellLayoutInfo { } export interface CodeCellLayoutChangeEvent { - source?: string; - editorHeight?: boolean; - commentHeight?: boolean; - outputHeight?: boolean; - outputShowMoreContainerHeight?: number; - totalHeight?: boolean; - outerWidth?: number; - font?: FontInfo; + readonly source?: string; + readonly editorHeight?: boolean; + readonly commentHeight?: boolean; + readonly outputHeight?: boolean; + readonly outputShowMoreContainerHeight?: number; + readonly totalHeight?: boolean; + readonly outerWidth?: number; + readonly font?: FontInfo; } export interface MarkupCellLayoutInfo { @@ -200,18 +200,18 @@ export enum CellLayoutContext { } export interface MarkupCellLayoutChangeEvent { - font?: FontInfo; - outerWidth?: number; - editorHeight?: number; - previewHeight?: number; + readonly font?: FontInfo; + readonly outerWidth?: number; + readonly editorHeight?: number; + readonly previewHeight?: number; totalHeight?: number; - context?: CellLayoutContext; + readonly context?: CellLayoutContext; } export interface ICommonCellViewModelLayoutChangeInfo { - totalHeight?: boolean | number; - outerWidth?: number; - context?: CellLayoutContext; + readonly totalHeight?: boolean | number; + readonly outerWidth?: number; + readonly context?: CellLayoutContext; } export interface ICellViewModel extends IGenericCellViewModel { readonly model: NotebookCellTextModel; @@ -248,7 +248,7 @@ export interface ICellViewModel extends IGenericCellViewModel { getCellStatusBarItems(): INotebookCellStatusBarItem[]; getEditState(): CellEditState; updateEditState(state: CellEditState, source: string): void; - deltaModelDecorations(oldDecorations: string[], newDecorations: IModelDeltaDecoration[]): string[]; + deltaModelDecorations(oldDecorations: readonly string[], newDecorations: readonly IModelDeltaDecoration[]): string[]; getCellDecorationRange(id: string): Range | null; } @@ -289,13 +289,13 @@ export interface INotebookCellDecorationOptions { } export interface INotebookDeltaDecoration { - handle: number; - options: INotebookCellDecorationOptions; + readonly handle: number; + readonly options: INotebookCellDecorationOptions; } export interface INotebookDeltaCellStatusBarItems { - handle: number; - items: INotebookCellStatusBarItem[]; + readonly handle: number; + readonly items: readonly INotebookCellStatusBarItem[]; } @@ -338,7 +338,7 @@ export interface INotebookEditorCreationOptions { } export interface INotebookWebviewMessage { - message: unknown; + readonly message: unknown; } //#region Notebook View Model @@ -357,13 +357,13 @@ export interface INotebookEditorViewState { } export interface ICellModelDecorations { - ownerId: number; - decorations: string[]; + readonly ownerId: number; + readonly decorations: readonly string[]; } export interface ICellModelDeltaDecorations { - ownerId: number; - decorations: IModelDeltaDecoration[]; + readonly ownerId: number; + readonly decorations: readonly IModelDeltaDecoration[]; } export interface IModelDecorationsChangeAccessor { @@ -378,8 +378,8 @@ export type NotebookViewCellsSplice = [ ]; export interface INotebookViewCellsUpdateEvent { - synchronous: boolean; - splices: NotebookViewCellsSplice[]; + readonly synchronous: boolean; + readonly splices: readonly NotebookViewCellsSplice[]; } export interface INotebookViewModel { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 81de7c57db3..09d6ffab345 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -85,6 +85,7 @@ import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { NotebookPerfMarks } from 'vs/workbench/contrib/notebook/common/notebookPerformance'; import { BaseCellEditorOptions } from 'vs/workbench/contrib/notebook/browser/viewModel/cellEditorOptions'; +import { ILogService } from 'vs/platform/log/common/log'; const $ = DOM.$; @@ -255,6 +256,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD @INotebookExecutionService private readonly notebookExecutionService: INotebookExecutionService, @INotebookExecutionStateService notebookExecutionStateService: INotebookExecutionStateService, @IEditorProgressService private readonly editorProgressService: IEditorProgressService, + @ILogService private readonly logService: ILogService ) { super(); this.isEmbedded = creationOptions.isEmbedded ?? false; @@ -1408,7 +1410,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD } // update cell listener - e.splices.reverse().forEach(splice => { + [...e.splices].reverse().forEach(splice => { const [start, deleted, newCells] = splice; const deletedCells = this._localCellStateListeners.splice(start, deleted, ...newCells.map(cell => this._bindCellListener(cell))); @@ -1481,12 +1483,15 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD private async _warmupWithMarkdownRenderer(viewModel: NotebookViewModel, viewState: INotebookEditorViewState | undefined) { + this.logService.trace('NotebookEditorWidget.warmup', this.viewModel?.uri.toString()); await this._resolveWebview(); + this.logService.trace('NotebookEditorWidget.warmup - webview resolved'); // make sure that the webview is not visible otherwise users will see pre-rendered markdown cells in wrong position as the list view doesn't have a correct `top` offset yet this._webview!.element.style.visibility = 'hidden'; // warm up can take around 200ms to load markdown libraries, etc. await this._warmupViewport(viewModel, viewState); + this.logService.trace('NotebookEditorWidget.warmup - viewport warmed up'); // todo@rebornix @mjbvz, is this too complicated? @@ -1505,7 +1510,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._list.scrollTop = viewState?.scrollPosition?.top ?? 0; this._debug('finish initial viewport warmup and view state restore.'); this._webview!.element.style.visibility = 'visible'; - + this.logService.trace('NotebookEditorWidget.warmup - list view model attached, set to visible'); } private async _warmupViewport(viewModel: NotebookViewModel, viewState: INotebookEditorViewState | undefined) { diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookExecutionServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookExecutionServiceImpl.ts index de0c18f7638..e2d7a240ced 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookExecutionServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookExecutionServiceImpl.ts @@ -93,8 +93,13 @@ export class NotebookExecutionService implements INotebookExecutionService, IDis if (info.all.length === 0) { // no kernel at all const sourceActions = this._notebookKernelService.getSourceActions(); - if (sourceActions.length === 1) { - await sourceActions[0].runAction(); + const primaryActions = sourceActions.filter(action => action.isPrimary); + const action = sourceActions.length === 1 + ? sourceActions[0] + : (primaryActions.length === 1 ? primaryActions[0] : undefined); + + if (action) { + await action.runAction(); kernel = this._notebookKernelService.getSelectedOrSuggestedKernel(notebook); } } diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl.ts index a069a226017..dd2ab69fa3c 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl.ts @@ -53,6 +53,7 @@ class SourceAction extends Disposable implements ISourceAction { constructor( readonly action: IAction, + readonly isPrimary: boolean ) { super(); } @@ -133,17 +134,18 @@ export class NotebookKernelService extends Disposable implements INotebookKernel private _initSourceActions() { const loadActionsFromMenu = (menu: IMenu) => { const groups = menu.getActions({ shouldForwardArgs: true }); - const actions: IAction[] = []; + const sourceActions: [ISourceAction, IDisposable][] = []; groups.forEach(group => { - actions.push(...group[1]); - }); - this._sourceActions = actions.map(action => { - const sourceAction = new SourceAction(action); - const stateChangeListener = sourceAction.onDidChangeState(() => { - this._onDidChangeSourceActions.fire(); + const isPrimary = /^primary/.test(group[0]); + group[1].forEach(action => { + const sourceAction = new SourceAction(action, isPrimary); + const stateChangeListener = sourceAction.onDidChangeState(() => { + this._onDidChangeSourceActions.fire(); + }); + sourceActions.push([sourceAction, stateChangeListener]); }); - return [sourceAction, stateChangeListener]; }); + this._sourceActions = sourceActions; this._onDidChangeSourceActions.fire(); }; @@ -245,10 +247,8 @@ export class NotebookKernelService extends Disposable implements INotebookKernel const selectedId = this._notebookBindings.get(NotebookTextModelLikeId.str(notebook)); const selected = selectedId ? this._kernels.get(selectedId)?.kernel : undefined; const suggestions = kernels.filter(item => item.instanceAffinity > 1 && item.kernel !== selected).map(item => item.kernel); - if (!suggestions.length && all.length) { - suggestions.push(all[0]); - } - return { all, selected, suggestions }; + const hidden = kernels.filter(item => item.instanceAffinity < 0).map(item => item.kernel); + return { all, selected, suggestions, hidden }; } getSelectedOrSuggestedKernel(notebook: INotebookTextModel): INotebookKernel | undefined { diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts index 8497edd6c0f..bfcb770270e 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts @@ -46,7 +46,7 @@ export class CellExecutionPart extends CellPart { } private updateExecutionOrder(internalMetadata: NotebookCellInternalMetadata, forceClear = false): void { - if (this._notebookEditor.activeKernel?.implementsExecutionOrder) { + if (this._notebookEditor.activeKernel?.implementsExecutionOrder || (!this._notebookEditor.activeKernel && typeof internalMetadata.executionOrder === 'number')) { // If the executionOrder was just cleared, and the cell is executing, wait just a bit before clearing the view to avoid flashing if (typeof internalMetadata.executionOrder !== 'number' && !forceClear && !!this._notebookExecutionStateService.getCellExecution(this.currentCell!.uri)) { const renderingCell = this.currentCell; diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars.ts index 8266fa0adc8..2ac050ea828 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars.ts @@ -13,25 +13,31 @@ import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { createActionViewItem, createAndFillInActionBarActions, MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenu, IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CodiconActionViewItem } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellActionView'; import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; import { registerStickyScroll } from 'vs/workbench/contrib/notebook/browser/view/cellParts/stickyScroll'; -import { HiddenItemStrategy, MenuWorkbenchToolBar, WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; +import { WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; export class BetweenCellToolbar extends CellPart { - private _betweenCellToolbar!: MenuWorkbenchToolBar; + private _betweenCellToolbar!: ToolBar; constructor( private readonly _notebookEditor: INotebookEditorDelegate, _titleToolbarContainer: HTMLElement, private readonly _bottomCellToolbarContainer: HTMLElement, @IInstantiationService instantiationService: IInstantiationService, + @IContextMenuService contextMenuService: IContextMenuService, + @IContextKeyService contextKeyService: IContextKeyService, + @IMenuService menuService: IMenuService ) { super(); - this._betweenCellToolbar = this._register(instantiationService.createInstance(MenuWorkbenchToolBar, this._bottomCellToolbarContainer, this._notebookEditor.creationOptions.menuIds.cellInsertToolbar, { + + this._betweenCellToolbar = this._register(new ToolBar(this._bottomCellToolbarContainer, contextMenuService, { actionViewItemProvider: action => { if (action instanceof MenuItemAction) { if (this._notebookEditor.notebookOptions.getLayoutConfiguration().insertToolbarAlignment === 'center') { @@ -42,14 +48,22 @@ export class BetweenCellToolbar extends CellPart { } return undefined; - }, - toolbarOptions: { - primaryGroup: g => /^inline/.test(g), - }, - menuOptions: { - shouldForwardArgs: true } })); + + const menu = this._register(menuService.createMenu(this._notebookEditor.creationOptions.menuIds.cellInsertToolbar, contextKeyService)); + const updateActions = () => { + const actions = getCellToolbarActions(menu); + this._betweenCellToolbar.setActions(actions.primary, actions.secondary); + }; + + this._register(menu.onDidChange(() => updateActions())); + this._register(this._notebookEditor.notebookOptions.onDidChangeOptions((e) => { + if (e.insertToolbarAlignment) { + updateActions(); + } + })); + updateActions(); } updateContext(context: INotebookCellActionContext) { @@ -77,9 +91,9 @@ export interface ICssClassDelegate { } export class CellTitleToolbarPart extends CellPart { - private _toolbar: WorkbenchToolBar; + private _toolbar: ToolBar; private _titleMenu: IMenu; - private _deleteToolbar: WorkbenchToolBar; + private _deleteToolbar: ToolBar; private _deleteMenu: IMenu; private readonly _onDidUpdateActions: Emitter = this._register(new Emitter()); readonly onDidUpdateActions: Event = this._onDidUpdateActions.event; @@ -100,10 +114,15 @@ export class CellTitleToolbarPart extends CellPart { ) { super(); - this._toolbar = instantiationService.invokeFunction(accessor => createToolbar(accessor, toolbarContainer)); + this._toolbar = instantiationService.createInstance(WorkbenchToolBar, toolbarContainer, { + actionViewItemProvider: action => { + return createActionViewItem(instantiationService, action); + }, + renderDropdownAsChildElement: true + }); this._titleMenu = this._register(menuService.createMenu(toolbarId, contextKeyService)); - this._deleteToolbar = this._register(instantiationService.invokeFunction(accessor => createToolbar(accessor, toolbarContainer, 'cell-delete-toolbar', HiddenItemStrategy.Ignore))); + this._deleteToolbar = this._register(instantiationService.invokeFunction(accessor => createDeleteToolbar(accessor, toolbarContainer, 'cell-delete-toolbar'))); this._deleteMenu = this._register(menuService.createMenu(deleteToolbarId, contextKeyService)); if (!this._notebookEditor.creationOptions.isReadOnly) { const deleteActions = getCellToolbarActions(this._deleteMenu); @@ -189,15 +208,16 @@ function getCellToolbarActions(menu: IMenu): { primary: IAction[]; secondary: IA return result; } -function createToolbar(accessor: ServicesAccessor, container: HTMLElement, elementClass?: string, hiddenItemStrategy?: HiddenItemStrategy): WorkbenchToolBar { +function createDeleteToolbar(accessor: ServicesAccessor, container: HTMLElement, elementClass?: string): ToolBar { + const contextMenuService = accessor.get(IContextMenuService); + const keybindingService = accessor.get(IKeybindingService); const instantiationService = accessor.get(IInstantiationService); - - const toolbar = instantiationService.createInstance(WorkbenchToolBar, container, { + const toolbar = new ToolBar(container, contextMenuService, { + getKeyBinding: action => keybindingService.lookupKeybinding(action.id), actionViewItemProvider: action => { return createActionViewItem(instantiationService, action); }, - renderDropdownAsChildElement: true, - hiddenItemStrategy + renderDropdownAsChildElement: true }); if (elementClass) { diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCellRunToolbar.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCellRunToolbar.ts index 4a2908a4ebb..ab30afc1519 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCellRunToolbar.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCellRunToolbar.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { Action, IAction } from 'vs/base/common/actions'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { MarshalledId } from 'vs/base/common/marshallingIds'; @@ -10,7 +11,6 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { localize } from 'vs/nls'; import { DropdownWithPrimaryActionViewItem } from 'vs/platform/actions/browser/dropdownWithPrimaryActionViewItem'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { HiddenItemStrategy, WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { IMenu, IMenuService, MenuItemAction } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { InputFocusedContext } from 'vs/platform/contextkey/common/contextkeys'; @@ -24,7 +24,7 @@ import { registerStickyScroll } from 'vs/workbench/contrib/notebook/browser/view import { NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; export class RunToolbar extends CellPart { - private toolbar!: WorkbenchToolBar; + private toolbar!: ToolBar; private primaryMenu: IMenu; private secondaryMenu: IMenu; @@ -82,8 +82,7 @@ export class RunToolbar extends CellPart { const keybindingProvider = (action: IAction) => this.keybindingService.lookupKeybinding(action.id, executionContextKeyService); const executionContextKeyService = this._register(getCodeCellExecutionContextKeyService(contextKeyService)); - this.toolbar = this._register(this.instantiationService.createInstance(WorkbenchToolBar, container, { - hiddenItemStrategy: HiddenItemStrategy.Ignore, + this.toolbar = this._register(new ToolBar(container, this.contextMenuService, { getKeyBinding: keybindingProvider, actionViewItemProvider: _action => { actionViewItemDisposables.clear(); 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 583ba78776d..30da51aa51c 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -394,9 +394,8 @@ export class BackLayerWebView extends Disposable { ${coreDependencies}
- -
+ `; } 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 c6378729b39..c0f2e223fea 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -1043,12 +1043,15 @@ async function webviewPreloads(ctx: PreloadContext) { const event = rawEvent as ({ data: webviewMessages.ToWebviewMessage }); switch (event.data.type) { - case 'initializeMarkup': - await Promise.all(event.data.cells.map(info => viewModel.ensureMarkupCell(info))); - dimensionUpdater.updateImmediately(); - postNotebookMessage('initializedMarkup', {}); + case 'initializeMarkup': { + try { + await Promise.all(event.data.cells.map(info => viewModel.ensureMarkupCell(info))); + } finally { + dimensionUpdater.updateImmediately(); + postNotebookMessage('initializedMarkup', {}); + } break; - + } case 'createMarkupCell': viewModel.ensureMarkupCell(event.data.cell); break; @@ -1548,7 +1551,7 @@ async function webviewPreloads(ctx: PreloadContext) { } const cell = new MarkupCell(init.cellId, init.mime, init.content, top, init.metadata); - cell.element.style.visibility = visible ? 'visible' : 'hidden'; + cell.element.style.visibility = visible ? '' : 'hidden'; this._markupCells.set(init.cellId, cell); await cell.ready; @@ -1558,7 +1561,7 @@ async function webviewPreloads(ctx: PreloadContext) { public async ensureMarkupCell(info: webviewMessages.IMarkupCellInitialization): Promise { let cell = this._markupCells.get(info.cellId); if (cell) { - cell.element.style.visibility = info.visible ? 'visible' : 'hidden'; + cell.element.style.visibility = info.visible ? '' : 'hidden'; await cell.updateContentAndRender(info.content, info.metadata); } else { cell = await this.createMarkupCell(info, info.offset, info.visible); @@ -1741,6 +1744,7 @@ async function webviewPreloads(ctx: PreloadContext) { /// Internal field that holds text content private _content: { readonly value: string; readonly version: number; readonly metadata: NotebookCellMetadata }; + private _isDisposed = false; private renderTaskAbort?: AbortController; constructor(id: string, mime: string, content: string, top: number, metadata: NotebookCellMetadata) { @@ -1748,8 +1752,12 @@ async function webviewPreloads(ctx: PreloadContext) { this.id = id; this._content = { value: content, version: 0, metadata: metadata }; - let resolveReady: () => void; - this.ready = new Promise(r => resolveReady = r); + let resolve: () => void; + let reject: () => void; + this.ready = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); let cachedData: { readonly version: number; readonly value: Uint8Array } | undefined; this.outputItem = Object.freeze({ @@ -1801,12 +1809,15 @@ async function webviewPreloads(ctx: PreloadContext) { this.addEventListeners(); this.updateContentAndRender(this._content.value, this._content.metadata).then(() => { - resizeObserver.observe(this.element, this.id, false, this.id); - resolveReady(); - }); + if (!this._isDisposed) { + resizeObserver.observe(this.element, this.id, false, this.id); + } + resolve(); + }, () => reject()); } public dispose() { + this._isDisposed = true; this.renderTaskAbort?.abort(); this.renderTaskAbort = undefined; } @@ -1900,7 +1911,7 @@ async function webviewPreloads(ctx: PreloadContext) { } public show(top: number, newContent: string | undefined, metadata: NotebookCellMetadata | undefined): void { - this.element.style.visibility = 'visible'; + this.element.style.visibility = ''; this.element.style.top = `${top}px`; if (typeof newContent === 'string' || metadata) { this.updateContentAndRender(newContent ?? this._content.value, metadata ?? this._content.metadata); @@ -1914,7 +1925,7 @@ async function webviewPreloads(ctx: PreloadContext) { } public unhide() { - this.element.style.visibility = 'visible'; + this.element.style.visibility = ''; this.updateMarkupDimensions(); } @@ -1993,7 +2004,7 @@ async function webviewPreloads(ctx: PreloadContext) { await outputElement.render(data.content, preloadErrors, signal); // don't hide until after this step so that the height is right - outputElement.element.style.visibility = data.initiallyHidden ? 'hidden' : 'visible'; + outputElement.element.style.visibility = data.initiallyHidden ? 'hidden' : ''; } public clearOutput(outputId: string, rendererId: string | undefined) { @@ -2009,7 +2020,7 @@ async function webviewPreloads(ctx: PreloadContext) { return; } - this.element.style.visibility = 'visible'; + this.element.style.visibility = ''; this.element.style.top = `${top}px`; dimensionUpdater.updateHeight(outputId, outputContainer.element.offsetHeight, { @@ -2041,7 +2052,7 @@ async function webviewPreloads(ctx: PreloadContext) { this.outputElements.get(request.outputId)?.updateScroll(request.outputOffset); if (request.forceDisplay) { - this.element.style.visibility = 'visible'; + this.element.style.visibility = ''; } } } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index 9f5be84ed97..dc2265e5f26 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -373,7 +373,7 @@ export abstract class BaseCellViewModel extends Disposable { this._resolvedDecorations.delete(decorationId); } - deltaModelDecorations(oldDecorations: string[], newDecorations: model.IModelDeltaDecoration[]): string[] { + deltaModelDecorations(oldDecorations: readonly string[], newDecorations: readonly model.IModelDeltaDecoration[]): string[] { oldDecorations.forEach(id => { this.removeModelDecoration(id); }); @@ -427,7 +427,7 @@ export abstract class BaseCellViewModel extends Disposable { return ret; } - deltaCellStatusBarItems(oldItems: string[], newItems: INotebookCellStatusBarItem[]): string[] { + deltaCellStatusBarItems(oldItems: readonly string[], newItems: readonly INotebookCellStatusBarItem[]): string[] { oldItems.forEach(id => { const item = this._cellStatusBarItems.get(id); if (item) { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts index a8ef03bcdcb..099e660c7cb 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -24,8 +24,10 @@ import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebo export class CodeCellViewModel extends BaseCellViewModel implements ICellViewModel { readonly cellKind = CellKind.Code; + protected readonly _onLayoutInfoRead = this._register(new Emitter()); readonly onLayoutInfoRead = this._onLayoutInfoRead.event; + protected readonly _onDidChangeOutputs = this._register(new Emitter()); readonly onDidChangeOutputs = this._onDidChangeOutputs.event; @@ -288,10 +290,11 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod }; } - state.totalHeight = this.layoutInfo.totalHeight !== originalLayout.totalHeight; - state.source = source; - - this._fireOnDidChangeLayout(state); + this._fireOnDidChangeLayout({ + ...state, + totalHeight: this.layoutInfo.totalHeight !== originalLayout.totalHeight, + source, + }); } private _fireOnDidChangeLayout(state: CodeCellLayoutChangeEvent) { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts index 4229a1a96d9..8dcc07dddf2 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts @@ -839,7 +839,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD private _deltaModelDecorationsImpl(oldDecorations: ICellModelDecorations[], newDecorations: ICellModelDeltaDecorations[]): ICellModelDecorations[] { - const mapping = new Map(); + const mapping = new Map(); oldDecorations.forEach(oldDecoration => { const ownerId = oldDecoration.ownerId; diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts index 35506ece47f..f8e26930397 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts @@ -125,7 +125,7 @@ export class NotebookEditorContextKeys { this._updateForInstalledExtension(); this._viewModelDisposables.add(this._editor.onDidChangeViewCells(e => { - e.splices.reverse().forEach(splice => { + [...e.splices].reverse().forEach(splice => { const [start, deleted, newCells] = splice; const deletedCellOutputStates = this._cellOutputsListeners.splice(start, deleted, ...newCells.map(addCellOutputsListener)); dispose(deletedCellOutputStates); diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelActionViewItem.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelActionViewItem.ts index 958a59df428..c461cd64a72 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelActionViewItem.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelActionViewItem.ts @@ -86,6 +86,9 @@ export class NotebooKernelActionViewItem extends ActionViewItem { if (sourceActions.length === 1) { // exact one action this._updateActionFromSourceAction(sourceActions[0], false); + } else if (sourceActions.filter(sourceAction => sourceAction.isPrimary).length === 1) { + // exact one primary action + this._updateActionFromSourceAction(sourceActions.filter(sourceAction => sourceAction.isPrimary)[0], false); } else { this._action.class = ThemeIcon.asClassName(selectKernelIcon); this._action.label = localize('select', "Select Kernel"); @@ -96,7 +99,9 @@ export class NotebooKernelActionViewItem extends ActionViewItem { private _updateActionFromKernelInfo(info: INotebookKernelMatchResult): void { this._action.enabled = true; this._action.class = ThemeIcon.asClassName(selectKernelIcon); - const selectedOrSuggested = info.selected ?? (info.suggestions.length === 1 ? info.suggestions[0] : undefined); + const selectedOrSuggested = info.selected + ?? (info.suggestions.length === 1 ? info.suggestions[0] : undefined) + ?? (info.all.length === 1 ? info.all[0] : undefined); if (selectedOrSuggested) { // selected or suggested kernel this._action.label = this._generateKenrelLabel(selectedOrSuggested); diff --git a/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts b/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts index a32d1ba2815..e674058c02b 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts @@ -20,6 +20,7 @@ export interface INotebookKernelMatchResult { readonly selected: INotebookKernel | undefined; readonly suggestions: INotebookKernel[]; readonly all: INotebookKernel[]; + readonly hidden: INotebookKernel[]; } @@ -68,6 +69,7 @@ export interface INotebookProxyKernelChangeEvent extends INotebookKernelChangeEv export interface ISourceAction { readonly action: IAction; readonly onDidChangeState: Event; + readonly isPrimary?: boolean; execution: Promise | undefined; runAction: () => Promise; } diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/contributedStatusBarItemController.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/contributedStatusBarItemController.test.ts new file mode 100644 index 00000000000..b37d157699b --- /dev/null +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/contributedStatusBarItemController.test.ts @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { CancellationToken } from 'vs/base/common/cancellation'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { ContributedStatusBarItemController } from 'vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/contributedStatusBarItemController'; +import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; +import { CellKind, INotebookCellStatusBarItemProvider } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; + +suite('Notebook Statusbar', () => { + const testDisposables = new DisposableStore(); + + teardown(() => { + testDisposables.clear(); + }); + + test('Calls item provider', async function () { + await withTestNotebook( + [ + ['var b = 1;', 'javascript', CellKind.Code, [], {}], + ['# header a', 'markdown', CellKind.Markup, [], {}], + ], + async (editor, viewModel, accessor) => { + const cellStatusbarSvc = accessor.get(INotebookCellStatusBarService); + testDisposables.add(accessor.createInstance(ContributedStatusBarItemController, editor)); + + const provider = testDisposables.add(new class extends Disposable implements INotebookCellStatusBarItemProvider { + private provideCalls = 0; + + private _onProvideCalled = this._register(new Emitter()); + public onProvideCalled = this._onProvideCalled.event; + + public _onDidChangeStatusBarItems = this._register(new Emitter()); + public onDidChangeStatusBarItems = this._onDidChangeStatusBarItems.event; + + async provideCellStatusBarItems(_uri: URI, index: number, _token: CancellationToken) { + if (index === 0) { + this.provideCalls++; + this._onProvideCalled.fire(this.provideCalls); + } + + return { items: [] }; + } + + viewType = editor.textModel.viewType; + }); + const providePromise1 = asPromise(provider.onProvideCalled); + testDisposables.add(cellStatusbarSvc.registerCellStatusBarItemProvider(provider)); + assert.strictEqual(await providePromise1, 1, 'should call provider on registration'); + + const providePromise2 = asPromise(provider.onProvideCalled); + const cell0 = editor.textModel.cells[0]; + cell0.metadata = { ...cell0.metadata, ...{ newMetadata: true } }; + assert.strictEqual(await providePromise2, 2, 'should call provider on registration'); + + const providePromise3 = asPromise(provider.onProvideCalled); + cell0.language = 'newlanguage'; + assert.strictEqual(await providePromise3, 3, 'should call provider on registration'); + + const providePromise4 = asPromise(provider.onProvideCalled); + provider._onDidChangeStatusBarItems.fire(); + assert.strictEqual(await providePromise4, 4, 'should call provider on registration'); + }); + }); +}); + +async function asPromise(event: Event, timeout = 5000): Promise { + const error = new Error('asPromise TIMEOUT reached'); + return new Promise((resolve, reject) => { + const handle = setTimeout(() => { + sub.dispose(); + reject(error); + }, timeout); + + const sub = event(e => { + clearTimeout(handle); + sub.dispose(); + resolve(e); + }); + }); +} diff --git a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts index e3deb9f4592..718588d06fe 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts @@ -9,22 +9,26 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { NotImplementedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ResourceMap } from 'vs/base/common/map'; import { Mimes } from 'vs/base/common/mime'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; -import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { ILanguageService } from 'vs/editor/common/languages/language'; +import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { LanguageService } from 'vs/editor/common/services/languageService'; import { IModelService } from 'vs/editor/common/services/model'; import { ModelService } from 'vs/editor/common/services/modelService'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { TestClipboardService } from 'vs/platform/clipboard/test/common/testClipboardService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { ContextKeyService } from 'vs/platform/contextkey/browser/contextKeyService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { IListService, ListService } from 'vs/platform/list/browser/listService'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; @@ -37,25 +41,23 @@ import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/work import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { CellFindMatchWithIndex, IActiveNotebookEditorDelegate, IBaseCellEditorOptions, ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/browser/services/notebookCellStatusBarServiceImpl'; import { ListViewInfoAccessor, NotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; import { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl'; import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; import { CellKind, CellUri, INotebookDiffEditorModel, INotebookEditorModel, INotebookSearchOptions, IOutputDto, IResolvedNotebookEditorModel, NotebookCellExecutionState, NotebookCellMetadata, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellExecuteUpdate, ICellExecutionComplete, ICellExecutionStateChangedEvent, INotebookCellExecution, INotebookExecutionStateService, INotebookFailStateChangedEvent } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOptions'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { TextModelResolverService } from 'vs/workbench/services/textmodelResolver/common/textModelResolverService'; +import { IWorkingCopySaveEvent } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { TestWorkspaceTrustRequestService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; import { TestLayoutService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; -import { ResourceMap } from 'vs/base/common/map'; -import { TestClipboardService } from 'vs/platform/clipboard/test/common/testClipboardService'; -import { IWorkingCopySaveEvent } from 'vs/workbench/services/workingCopy/common/workingCopy'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; export class TestCell extends NotebookCellTextModel { constructor( @@ -176,6 +178,7 @@ export function setupInstantiationService(disposables = new DisposableStore()) { instantiationService.stub(IWorkspaceTrustRequestService, new TestWorkspaceTrustRequestService(true)); instantiationService.stub(INotebookExecutionStateService, new TestNotebookExecutionStateService()); instantiationService.stub(IKeybindingService, new MockKeybindingService()); + instantiationService.stub(INotebookCellStatusBarService, new NotebookCellStatusBarService()); return instantiationService; } @@ -260,6 +263,7 @@ function _createTestNotebookEditor(instantiationService: TestInstantiationServic override cellAt(index: number) { return viewModel.cellAt(index)!; } override getCellIndex(cell: ICellViewModel) { return viewModel.getCellIndex(cell); } override getCellsInRange(range?: ICellRange) { return viewModel.getCellsInRange(range); } + override getCellByHandle(handle: number) { return viewModel.getCellByHandle(handle); } override getNextVisibleCellIndex(index: number) { return viewModel.getNextVisibleCellIndex(index); } getControl() { return this; } override get onDidChangeSelection() { return viewModel.onDidChangeSelection as Event; } @@ -270,6 +274,8 @@ function _createTestNotebookEditor(instantiationService: TestInstantiationServic return findMatches; } override deltaCellDecorations() { return []; } + override onDidChangeVisibleRanges = Event.None; + override visibleRanges: ICellRange[] = [{ start: 0, end: 100 }]; }; return { editor: notebookEditor, viewModel }; diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 5272b1f6564..69f441beb3d 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -699,7 +699,7 @@ export class SettingsEditor2 extends EditorPane { } } - if (!recursed && revealFailed) { + if (!recursed && (!targetElement || revealFailed)) { // We'll call this event handler again after clearing the search query, // so that more settings show up in the list. const p = this.triggerSearch(''); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index 7f0f263a22b..834aab26324 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -23,7 +23,7 @@ import { Color, RGBA } from 'vs/base/common/color'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { Disposable, DisposableStore, dispose, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, dispose, isDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { isIOS } from 'vs/base/common/platform'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; import { isDefined, isUndefinedOrNull } from 'vs/base/common/types'; @@ -751,7 +751,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre constructor( private readonly settingActions: IAction[], - private readonly disposableActionFactory: (setting: ISetting) => Action[], + private readonly disposableActionFactory: (setting: ISetting) => IAction[], @IThemeService protected readonly _themeService: IThemeService, @IContextViewService protected readonly _contextViewService: IContextViewService, @IOpenerService protected readonly _openerService: IOpenerService, @@ -895,7 +895,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre template.context = element; template.toolbar.context = element; const actions = this.disposableActionFactory(element.setting); - actions.forEach(a => template.elementDisposables?.add(a)); + actions.forEach(a => isDisposable(a) && template.elementDisposables?.add(a)); template.toolbar.setActions([], [...this.settingActions, ...actions]); const setting = element.setting; @@ -2025,7 +2025,7 @@ export class SettingTreeRenderers { ]; } - private getActionsForSetting(setting: ISetting): Action[] { + private getActionsForSetting(setting: ISetting): IAction[] { const enableSync = this._userDataSyncEnablementService.isEnabled(); return enableSync && !setting.disallowSyncIgnore ? [ diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 9f7d3bad110..b087ee0bd88 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -1006,7 +1006,7 @@ export class TunnelPanel extends ViewPane { event.browserEvent.preventDefault(); event.browserEvent.stopPropagation(); - const node: ITunnelItem | undefined = event.element; + const node: TunnelItem | undefined = event.element; if (node) { this.table.setFocus([this.table.indexOf(node)]); @@ -1043,7 +1043,7 @@ export class TunnelPanel extends ViewPane { this.table.domFocus(); } }, - getActionsContext: () => node, + getActionsContext: () => node?.strip(), actionRunner }); } @@ -1287,7 +1287,7 @@ export namespace OpenPortInBrowserAction { export function handler(): ICommandHandler { return async (accessor, arg) => { let key: string | undefined; - if (arg instanceof TunnelItem) { + if (isITunnelItem(arg)) { key = makeAddress(arg.remoteHost, arg.remotePort); } else if (arg.tunnelRemoteHost && arg.tunnelRemotePort) { key = makeAddress(arg.tunnelRemoteHost, arg.tunnelRemotePort); @@ -1316,7 +1316,7 @@ export namespace OpenPortInPreviewAction { export function handler(): ICommandHandler { return async (accessor, arg) => { let key: string | undefined; - if (arg instanceof TunnelItem) { + if (isITunnelItem(arg)) { key = makeAddress(arg.remoteHost, arg.remotePort); } else if (arg.tunnelRemoteHost && arg.tunnelRemotePort) { key = makeAddress(arg.tunnelRemoteHost, arg.tunnelRemotePort); @@ -1498,7 +1498,7 @@ namespace ChangeLocalPortAction { namespace ChangeTunnelPrivacyAction { export function handler(privacyId: string): ICommandHandler { return async (accessor, arg) => { - if (arg instanceof TunnelItem) { + if (isITunnelItem(arg)) { const remoteExplorerService = accessor.get(IRemoteExplorerService); await remoteExplorerService.close({ host: arg.remoteHost, port: arg.remotePort }); return remoteExplorerService.forward({ @@ -1521,7 +1521,7 @@ namespace SetTunnelProtocolAction { export const LABEL_HTTPS = nls.localize('remote.tunnel.protocolHttps', "HTTPS"); async function handler(arg: any, protocol: TunnelProtocol, remoteExplorerService: IRemoteExplorerService) { - if (arg instanceof TunnelItem) { + if (isITunnelItem(arg)) { const attributes: Partial = { protocol }; diff --git a/src/vs/workbench/contrib/scm/browser/activity.ts b/src/vs/workbench/contrib/scm/browser/activity.ts index 75929726a57..5c3774a0949 100644 --- a/src/vs/workbench/contrib/scm/browser/activity.ts +++ b/src/vs/workbench/contrib/scm/browser/activity.ts @@ -16,7 +16,6 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { EditorResourceAccessor } from 'vs/workbench/common/editor'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { stripIcons } from 'vs/base/common/iconLabels'; import { Schemas } from 'vs/base/common/network'; import { Iterable } from 'vs/base/common/iterator'; @@ -155,13 +154,10 @@ export class SCMStatusController implements IWorkbenchContribution { const command = commands[index]; const tooltip = `${label}${command.tooltip ? ` - ${command.tooltip}` : ''}`; - let ariaLabel = stripIcons(command.title).trim(); - ariaLabel = ariaLabel ? `${ariaLabel}, ${label}` : label; - disposables.add(this.statusbarService.addEntry({ name: localize('status.scm', "Source Control"), text: command.title, - ariaLabel: `${ariaLabel}${command.tooltip ? ` - ${command.tooltip}` : ''}`, + ariaLabel: tooltip, tooltip, command: command.id ? command : undefined }, `status.scm.${index}`, MainThreadStatusBarAlignment.LEFT, 10000 - index)); diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index f8879f51475..0ff79b1dc49 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -383,5 +383,5 @@ MenuRegistry.appendMenuItem(MenuId.SCMSourceControl, { when: ContextKeyExpr.equals('scmProviderHasRootUri', true) }); -registerSingleton(ISCMService, SCMService, false); -registerSingleton(ISCMViewService, SCMViewService, false); +registerSingleton(ISCMService, SCMService, true); +registerSingleton(ISCMViewService, SCMViewService, true); diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index 3142f496e6b..6cedf1989b2 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -40,7 +40,7 @@ import { ExplorerFolderContext, ExplorerRootContext, FilesExplorerFocusCondition import { AnythingQuickAccessProvider } from 'vs/workbench/contrib/search/browser/anythingQuickAccess'; import { registerContributions as replaceContributions } from 'vs/workbench/contrib/search/browser/replaceContributions'; import { cancelSearch, clearHistoryCommand, clearSearchResults, CloseReplaceAction, collapseDeepestExpandedLevel, copyAllCommand, copyMatchCommand, copyPathCommand, expandAll, FindInFilesCommand, FocusNextInputAction, FocusNextSearchResultAction, FocusPreviousInputAction, FocusPreviousSearchResultAction, focusSearchListCommand, getSearchView, openSearchView, refreshSearch, RemoveAction, ReplaceAction, ReplaceAllAction, ReplaceAllInFolderAction, ReplaceInFilesAction, toggleCaseSensitiveCommand, togglePreserveCaseCommand, toggleRegexCommand, ToggleSearchOnTypeAction, toggleWholeWordCommand } from 'vs/workbench/contrib/search/browser/searchActions'; -import { searchClearIcon, searchCollapseAllIcon, searchExpandAllIcon, searchRefreshIcon, searchStopIcon, searchViewIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; +import { searchClearIcon, searchCollapseAllIcon, searchExpandAllIcon, searchRefreshIcon, searchStopIcon, searchShowAsTree, searchViewIcon, searchShowAsList } from 'vs/workbench/contrib/search/browser/searchIcons'; import { SearchView } from 'vs/workbench/contrib/search/browser/searchView'; import { registerContributions as searchWidgetContributions } from 'vs/workbench/contrib/search/browser/searchWidget'; import { SymbolsQuickAccessProvider } from 'vs/workbench/contrib/search/browser/symbolsQuickAccess'; @@ -54,7 +54,7 @@ import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEd import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; -import { ISearchConfiguration, SearchSortOrder, SEARCH_EXCLUDE_CONFIG, VIEWLET_ID, VIEW_ID } from 'vs/workbench/services/search/common/search'; +import { ISearchConfiguration, SearchSortOrder, SEARCH_EXCLUDE_CONFIG, VIEWLET_ID, ViewMode, VIEW_ID } from 'vs/workbench/services/search/common/search'; import { Extensions, IConfigurationMigrationRegistry } from 'vs/workbench/common/configuration'; registerSingleton(ISearchWorkbenchService, SearchWorkbenchService, true); @@ -105,7 +105,14 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const searchView = getSearchView(accessor.get(IViewsService)); if (searchView) { const tree: WorkbenchObjectTree = searchView.getControl(); - searchView.open(tree.getFocus()[0], false, false, true); + const viewer = searchView.getControl(); + const focus = tree.getFocus()[0]; + + if (focus instanceof FolderMatch) { + viewer.toggleCollapsed(focus); + } else { + searchView.open(tree.getFocus()[0], false, false, true); + } } } }); @@ -489,6 +496,63 @@ registerAction2(class ClearSearchResultsAction extends Action2 { } }); +registerAction2(class ViewAsTreeAction extends Action2 { + constructor() { + super({ + id: 'search.action.viewAsTree', + title: { + value: nls.localize('ViewAsTreeAction.label', "View as Tree"), + original: 'View as Tree' + }, + category, + icon: searchShowAsTree, + f1: true, + precondition: ContextKeyExpr.and(Constants.HasSearchResults, Constants.InTreeViewKey.toNegated()), + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 3, + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), Constants.InTreeViewKey.toNegated()), + }] + }); + } + run(accessor: ServicesAccessor, ...args: any[]) { + const searchView = getSearchView(accessor.get(IViewsService)); + if (searchView) { + searchView.setTreeView(true); + } + } +}); + + +registerAction2(class ViewAsListAction extends Action2 { + constructor() { + super({ + id: 'search.action.viewAsList', + title: { + value: nls.localize('ViewAsListAction.label', "View as List"), + original: 'View as List' + }, + category, + icon: searchShowAsList, + f1: true, + precondition: ContextKeyExpr.and(Constants.HasSearchResults, Constants.InTreeViewKey), + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 3, + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), Constants.InTreeViewKey), + }] + }); + } + run(accessor: ServicesAccessor, ...args: any[]) { + const searchView = getSearchView(accessor.get(IViewsService)); + if (searchView) { + searchView.setTreeView(false); + } + } +}); + const RevealInSideBarForSearchResultsCommand: ICommandAction = { id: Constants.RevealInSideBarForSearchResults, title: nls.localize('revealInSideBar', "Reveal in Explorer View") @@ -1039,6 +1103,16 @@ configurationRegistry.registerConfiguration({ description: nls.localize('search.decorations.badges', "Controls whether search file decorations should use badges."), default: true }, + 'search.defaultViewMode': { + 'type': 'string', + 'enum': [ViewMode.Tree, ViewMode.List], + 'default': ViewMode.List, + 'enumDescriptions': [ + nls.localize('scm.defaultViewMode.tree', "Shows search results as a tree."), + nls.localize('scm.defaultViewMode.list', "Shows search results as a list.") + ], + 'description': nls.localize('search.defaultViewMode', "Controls the default search result view mode.") + }, } }); diff --git a/src/vs/workbench/contrib/search/browser/searchActions.ts b/src/vs/workbench/contrib/search/browser/searchActions.ts index 1af0a772f4d..cd458000ec8 100644 --- a/src/vs/workbench/contrib/search/browser/searchActions.ts +++ b/src/vs/workbench/contrib/search/browser/searchActions.ts @@ -23,7 +23,7 @@ import { SearchView } from 'vs/workbench/contrib/search/browser/searchView'; import * as Constants from 'vs/workbench/contrib/search/common/constants'; import { IReplaceService } from 'vs/workbench/contrib/search/common/replace'; import { ISearchHistoryService } from 'vs/workbench/contrib/search/common/searchHistoryService'; -import { arrayContainsElementOrParent, FileMatch, FolderMatch, FolderMatchWithResource, Match, RenderableMatch, searchComparer, searchMatchComparer, SearchResult } from 'vs/workbench/contrib/search/common/searchModel'; +import { arrayContainsElementOrParent, FileMatch, FolderMatch, FolderMatchNoRoot, FolderMatchWithResource, FolderMatchWorkspaceRoot, Match, RenderableMatch, searchComparer, searchMatchComparer, SearchResult } from 'vs/workbench/contrib/search/common/searchModel'; import { OpenEditorCommandId } from 'vs/workbench/contrib/searchEditor/browser/constants'; import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor'; import { OpenSearchEditorArgs } from 'vs/workbench/contrib/searchEditor/browser/searchEditor.contribution'; @@ -255,8 +255,6 @@ export function expandAll(accessor: ServicesAccessor) { if (searchView) { const viewer = searchView.getControl(); viewer.expandAll(); - viewer.domFocus(); - viewer.focusFirst(); } } @@ -291,29 +289,57 @@ export function collapseDeepestExpandedLevel(accessor: ServicesAccessor) { */ const navigator = viewer.navigate(); let node = navigator.first(); - let collapseFileMatchLevel = false; + let canCollapseFileMatchLevel = false; + let canCollapseFirstLevel = false; + if (node instanceof FolderMatch) { while (node = navigator.next()) { if (node instanceof Match) { - collapseFileMatchLevel = true; + canCollapseFileMatchLevel = true; break; } + if (searchView.isTreeLayoutViewVisible && !canCollapseFirstLevel) { + const immediateParent = node.parent(); + if (immediateParent instanceof FolderMatchWorkspaceRoot || immediateParent instanceof FolderMatchNoRoot) { + canCollapseFirstLevel = true; + } + } } } - if (collapseFileMatchLevel) { + if (canCollapseFileMatchLevel) { node = navigator.first(); do { if (node instanceof FileMatch) { viewer.collapse(node); } } while (node = navigator.next()); + } else if (canCollapseFirstLevel) { + node = navigator.first(); + if (node) { + do { + const immediateParent = node.parent(); + if (immediateParent instanceof FolderMatchWorkspaceRoot || immediateParent instanceof FolderMatchNoRoot) { + if (viewer.hasElement(immediateParent) && viewer.isCollapsed(immediateParent)) { + viewer.collapse(immediateParent, true); + } else { + viewer.collapseAll(); + } + } + } while (node = navigator.next()); + } } else { viewer.collapseAll(); } - viewer.domFocus(); - viewer.focusFirst(); + const firstFocusParent = viewer.getFocus()[0]?.parent(); + + if (firstFocusParent && (firstFocusParent instanceof FolderMatch || firstFocusParent instanceof FileMatch) && + viewer.hasElement(firstFocusParent) && viewer.isCollapsed(firstFocusParent)) { + viewer.domFocus(); + viewer.focusFirst(); + viewer.setSelection(viewer.getFocus()); + } } } @@ -370,9 +396,9 @@ export abstract class AbstractSearchAndReplaceAction extends Action { /** * Returns element to focus after removing the given element */ - getElementToFocusAfterRemoved(viewer: WorkbenchObjectTree, elementToRemove: RenderableMatch): RenderableMatch { + getElementToFocusAfterRemoved(viewer: WorkbenchObjectTree, elementToRemove: RenderableMatch, isTreeViewVisible: boolean): RenderableMatch { const elementToFocus = this.getNextElementAfterRemoved(viewer, elementToRemove); - return elementToFocus || this.getPreviousElementAfterRemoved(viewer, elementToRemove); + return elementToFocus || this.getPreviousElementAfterRemoved(viewer, elementToRemove, isTreeViewVisible); } getNextElementAfterRemoved(viewer: WorkbenchObjectTree, element: RenderableMatch): RenderableMatch { @@ -389,17 +415,16 @@ export abstract class AbstractSearchAndReplaceAction extends Action { return navigator.current(); } - getPreviousElementAfterRemoved(viewer: WorkbenchObjectTree, element: RenderableMatch): RenderableMatch { + getPreviousElementAfterRemoved(viewer: WorkbenchObjectTree, element: RenderableMatch, isTreeViewVisible: boolean): RenderableMatch { const navigator: ITreeNavigator = viewer.navigate(element); let previousElement = navigator.previous(); - // Hence take the previous element. - const parent = element.parent(); + const parent = getVisibleParent(element, isTreeViewVisible); if (parent === previousElement) { previousElement = navigator.previous(); } - if (parent instanceof FileMatch && parent.parent() === previousElement) { + if (parent instanceof FileMatch && getVisibleParent(parent, isTreeViewVisible) === previousElement) { previousElement = navigator.previous(); } @@ -425,8 +450,8 @@ class ReplaceActionRunner { constructor( private viewer: WorkbenchObjectTree, private viewlet: SearchView | undefined, - private getElementToFocusAfterRemoved: (viewer: WorkbenchObjectTree, lastElementToBeRemoved: RenderableMatch) => RenderableMatch, - private getPreviousElementAfterRemoved: (viewer: WorkbenchObjectTree, element: RenderableMatch) => RenderableMatch, + private getElementToFocusAfterRemoved: (viewer: WorkbenchObjectTree, lastElementToBeRemoved: RenderableMatch, isTreeViewVisible: boolean) => RenderableMatch, + private getPreviousElementAfterRemoved: (viewer: WorkbenchObjectTree, element: RenderableMatch, isTreeViewVisible: boolean) => RenderableMatch, // Services @IReplaceService private readonly replaceService: IReplaceService, @IEditorService private readonly editorService: IEditorService, @@ -440,7 +465,8 @@ class ReplaceActionRunner { const opInfo = getElementsToOperateOnInfo(this.viewer, element, this.configurationService.getValue('search')); const elementsToReplace = opInfo.elements; - const searchResult = getSearchView(this.viewsService)?.searchResult; + const searchView = getSearchView(this.viewsService); + const searchResult = searchView?.searchResult; if (searchResult) { searchResult.batchReplace(elementsToReplace); @@ -455,7 +481,7 @@ class ReplaceActionRunner { this.viewer.setFocus([elementToFocus], getSelectionKeyboardEvent()); this.viewer.setSelection([elementToFocus], getSelectionKeyboardEvent()); } - const elementToShowReplacePreview = this.getElementToShowReplacePreview(elementToFocus, currentBottomFocusElement); + const elementToShowReplacePreview = this.getElementToShowReplacePreview(elementToFocus, currentBottomFocusElement, searchView?.isTreeLayoutViewVisible ?? false); this.viewer.domFocus(); @@ -467,7 +493,7 @@ class ReplaceActionRunner { } return; } else { - const nextFocusElement = this.getElementToFocusAfterRemoved(this.viewer, currentBottomFocusElement); + const nextFocusElement = this.getElementToFocusAfterRemoved(this.viewer, currentBottomFocusElement, searchView?.isTreeLayoutViewVisible ?? false); if (nextFocusElement) { this.viewer.setFocus([nextFocusElement], getSelectionKeyboardEvent()); @@ -511,11 +537,11 @@ class ReplaceActionRunner { return elementToFocus!; } - private getElementToShowReplacePreview(elementToFocus: RenderableMatch, currBottomElem: RenderableMatch): Match | null { + private getElementToShowReplacePreview(elementToFocus: RenderableMatch, currBottomElem: RenderableMatch, isTreeViewVisible: boolean): Match | null { if (this.hasSameParent(elementToFocus, currBottomElem)) { return elementToFocus; } - const previousElement = this.getPreviousElementAfterRemoved(this.viewer, elementToFocus); + const previousElement = this.getPreviousElementAfterRemoved(this.viewer, elementToFocus, isTreeViewVisible); if (this.hasSameParent(previousElement, currBottomElem)) { return previousElement; } @@ -559,7 +585,7 @@ export class RemoveAction extends AbstractSearchAndReplaceAction { override async run(): Promise { const opInfo = getElementsToOperateOnInfo(this.viewer, this.element, this.configurationService.getValue('search')); const elementsToRemove = opInfo.elements; - + const searchView = getSearchView(this.viewsService); if (elementsToRemove.length === 0) { return; } @@ -567,7 +593,7 @@ export class RemoveAction extends AbstractSearchAndReplaceAction { if (opInfo.mustReselect) { for (const currentElement of elementsToRemove) { const nextFocusElement = !currentElement || currentElement instanceof SearchResult || arrayContainsElementOrParent(currentElement, elementsToRemove) ? - this.getElementToFocusAfterRemoved(this.viewer, currentElement) : + this.getElementToFocusAfterRemoved(this.viewer, currentElement, searchView?.isTreeLayoutViewVisible ?? false) : null; if (nextFocusElement && !arrayContainsElementOrParent(nextFocusElement, elementsToRemove)) { this.viewer.reveal(nextFocusElement); @@ -578,7 +604,7 @@ export class RemoveAction extends AbstractSearchAndReplaceAction { } } - const searchResult = getSearchView(this.viewsService)?.searchResult; + const searchResult = searchView?.searchResult; if (searchResult) { searchResult.batchRemove(elementsToRemove); @@ -690,7 +716,13 @@ function matchToString(match: Match, indent = 0): string { return formattedLines.join('\n'); } - +function fileFolderMatchToString(match: FileMatch | FolderMatch | FolderMatchWithResource, labelService: ILabelService): { text: string; count: number } { + if (match instanceof FileMatch) { + return fileMatchToString(match, labelService); + } else { + return folderMatchToString(match, labelService); + } +} const lineDelimiter = isWindows ? '\r\n' : '\n'; function fileMatchToString(fileMatch: FileMatch, labelService: ILabelService): { text: string; count: number } { const matchTextRows = fileMatch.matches() @@ -704,19 +736,19 @@ function fileMatchToString(fileMatch: FileMatch, labelService: ILabelService): { } function folderMatchToString(folderMatch: FolderMatchWithResource | FolderMatch, labelService: ILabelService): { text: string; count: number } { - const fileResults: string[] = []; + const results: string[] = []; let numMatches = 0; const matches = folderMatch.matches().sort(searchMatchComparer); matches.forEach(match => { - const fileResult = fileMatchToString(match, labelService); - numMatches += fileResult.count; - fileResults.push(fileResult.text); + const result = fileFolderMatchToString(match, labelService); + numMatches += result.count; + results.push(result.text); }); return { - text: fileResults.join(lineDelimiter + lineDelimiter), + text: results.join(lineDelimiter + lineDelimiter), count: numMatches }; } @@ -805,3 +837,7 @@ function getElementsToOperateOnInfo(viewer: WorkbenchObjectTree 1 ? nls.localize('searchFileMatches', "{0} files found", count) : nls.localize('searchFileMatch', "{0} file found", count)); @@ -187,7 +187,7 @@ export class FileMatchRenderer extends Disposable implements ITreeRenderer('search').decorations; - templateData.label.setFile(fileMatch.resource, { hideIcon: false, fileDecorations: { colors: decorationConfig.colors, badges: decorationConfig.badges } }); + templateData.label.setFile(fileMatch.resource, { hidePath: this.searchView.isTreeLayoutViewVisible && !(fileMatch.parent() instanceof FolderMatchNoRoot), hideIcon: false, fileDecorations: { colors: decorationConfig.colors, badges: decorationConfig.badges } }); const count = fileMatch.count(); templateData.badge.setCount(count); templateData.badge.setTitleFormat(count > 1 ? nls.localize('searchMatches', "{0} matches found", count) : nls.localize('searchMatch', "{0} match found", count)); @@ -315,9 +315,10 @@ export class SearchAccessibilityProvider implements IListAccessibilityProvider total + current.count(), 0); return element.resource ? - nls.localize('folderMatchAriaLabel', "{0} matches in folder root {1}, Search result", element.count(), element.name()) : - nls.localize('otherFilesAriaLabel', "{0} matches outside of the workspace, Search result", element.count()); + nls.localize('folderMatchAriaLabel', "{0} matches in folder root {1}, Search result", count, element.name()) : + nls.localize('otherFilesAriaLabel', "{0} matches outside of the workspace, Search result", count); } if (element instanceof FileMatch) { diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 7fa91c302c0..e209f70f451 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -77,7 +77,7 @@ import { createEditorFromSearchResult } from 'vs/workbench/contrib/searchEditor/ import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IPreferencesService, ISettingsEditorOptions } from 'vs/workbench/services/preferences/common/preferences'; import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/services/search/common/queryBuilder'; -import { IPatternInfo, ISearchComplete, ISearchConfiguration, ISearchConfigurationProperties, ITextQuery, SearchCompletionExitCode, SearchSortOrder, TextSearchCompleteMessageType } from 'vs/workbench/services/search/common/search'; +import { IPatternInfo, ISearchComplete, ISearchConfiguration, ISearchConfigurationProperties, ITextQuery, SearchCompletionExitCode, SearchSortOrder, TextSearchCompleteMessageType, ViewMode } from 'vs/workbench/services/search/common/search'; import { TextSearchCompleteMessage } from 'vs/workbench/services/search/common/searchExtTypes'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; @@ -154,6 +154,8 @@ export class SearchView extends ViewPane { private treeAccessibilityProvider: SearchAccessibilityProvider; + private treeViewKey: IContextKey; + constructor( options: IViewPaneOptions, @IFileService private readonly fileService: IFileService, @@ -203,6 +205,7 @@ export class SearchView extends ViewPane { this.hasReplacePatternKey = Constants.ViewHasReplacePatternKey.bindTo(this.contextKeyService); this.hasFilePatternKey = Constants.ViewHasFilePatternKey.bindTo(this.contextKeyService); this.hasSomeCollapsibleResultKey = Constants.ViewHasSomeCollapsibleKey.bindTo(this.contextKeyService); + this.treeViewKey = Constants.InTreeViewKey.bindTo(this.contextKeyService); // scoped this.contextKeyService = this._register(this.contextKeyService.createScoped(this.container)); @@ -243,6 +246,23 @@ export class SearchView extends ViewPane { this.triggerQueryDelayer = this._register(new Delayer(0)); this.treeAccessibilityProvider = this.instantiationService.createInstance(SearchAccessibilityProvider, this.viewModel); + this.isTreeLayoutViewVisible = this.viewletState['view.treeLayout'] ?? (this.searchConfig.defaultViewMode === ViewMode.Tree); + } + + get isTreeLayoutViewVisible(): boolean { + return this.treeViewKey.get() ?? false; + } + + private set isTreeLayoutViewVisible(visible: boolean) { + this.treeViewKey.set(visible); + } + + setTreeView(visible: boolean): void { + if (visible === this.isTreeLayoutViewVisible) { + return; + } + this.isTreeLayoutViewVisible = visible; + this.refreshTree(); } private get state(): SearchUIState { @@ -558,18 +578,24 @@ export class SearchView extends ViewPane { private createFolderIterator(folderMatch: FolderMatch, collapseResults: ISearchConfigurationProperties['collapseResults']): Iterable> { const sortOrder = this.searchConfig.sortOrder; - const matches = folderMatch.matches().sort((a, b) => searchMatchComparer(a, b, sortOrder)); - return Iterable.map(matches, fileMatch => { - const children = this.createFileIterator(fileMatch); + const matchArray = this.isTreeLayoutViewVisible ? folderMatch.matches() : folderMatch.downstreamFileMatches(); + const matches = matchArray.sort((a, b) => searchMatchComparer(a, b, sortOrder)); + return Iterable.map(matches, match => { + let children; + if (match instanceof FileMatch) { + children = this.createFileIterator(match); + } else { + children = this.createFolderIterator(match, collapseResults); + } let nodeExists = true; - try { this.tree.getNode(fileMatch); } catch (e) { nodeExists = false; } + try { this.tree.getNode(match); } catch (e) { nodeExists = false; } const collapsed = nodeExists ? undefined : - (collapseResults === 'alwaysCollapse' || (fileMatch.matches().length > 10 && collapseResults !== 'alwaysExpand')); + (collapseResults === 'alwaysCollapse' || (match.count() > 10 && collapseResults !== 'alwaysExpand')); - return >{ element: fileMatch, children, collapsed }; + return >{ element: match, children, collapsed }; }); } @@ -1882,6 +1908,7 @@ export class SearchView extends ViewPane { const isReplaceShown = this.searchAndReplaceWidget.isReplaceShown(); this.viewletState['view.showReplace'] = isReplaceShown; + this.viewletState['view.treeLayout'] = this.isTreeLayoutViewVisible; this.viewletState['query.replaceText'] = isReplaceShown && this.searchWidget.getReplaceValue(); const history: ISearchHistoryValues = Object.create(null); diff --git a/src/vs/workbench/contrib/search/common/constants.ts b/src/vs/workbench/contrib/search/common/constants.ts index 63c8976ea9d..5a4dfd6ed45 100644 --- a/src/vs/workbench/contrib/search/common/constants.ts +++ b/src/vs/workbench/contrib/search/common/constants.ts @@ -49,3 +49,4 @@ export const ViewHasSearchPatternKey = new RawContextKey('viewHasSearch export const ViewHasReplacePatternKey = new RawContextKey('viewHasReplacePattern', false); export const ViewHasFilePatternKey = new RawContextKey('viewHasFilePattern', false); export const ViewHasSomeCollapsibleKey = new RawContextKey('viewHasSomeCollapsibleResult', false); +export const InTreeViewKey = new RawContextKey('inTreeView', false); diff --git a/src/vs/workbench/contrib/search/common/searchModel.ts b/src/vs/workbench/contrib/search/common/searchModel.ts index c58c9c8bad2..0aafd7dbff6 100644 --- a/src/vs/workbench/contrib/search/common/searchModel.ts +++ b/src/vs/workbench/contrib/search/common/searchModel.ts @@ -222,6 +222,7 @@ export class FileMatch extends Disposable implements IFileMatch { private _maxResults: number | undefined, private _parent: FolderMatch, private rawMatch: IFileMatch, + private _closestRoot: FolderMatchWorkspaceRoot | null, @IModelService private readonly modelService: IModelService, @IReplaceService private readonly replaceService: IReplaceService, @ILabelService private readonly labelService: ILabelService, @@ -235,6 +236,10 @@ export class FileMatch extends Disposable implements IFileMatch { this.createMatches(); } + get closestRoot(): FolderMatchWorkspaceRoot | null { + return this._closestRoot; + } + private createMatches(): void { const model = this.modelService.getModel(this._resource); if (model) { @@ -475,30 +480,37 @@ export interface IChangeEvent { export class FolderMatch extends Disposable { - private _onChange = this._register(new Emitter()); + protected _onChange = this._register(new Emitter()); readonly onChange: Event = this._onChange.event; private _onDispose = this._register(new Emitter()); readonly onDispose: Event = this._onDispose.event; - private _fileMatches: ResourceMap; - private _unDisposedFileMatches: ResourceMap; + protected _fileMatches: ResourceMap; + protected _folderMatches: ResourceMap; + protected _folderMatchesMap: TernarySearchTree; + protected _unDisposedFileMatches: ResourceMap; + protected _unDisposedFolderMatches: ResourceMap; private _replacingAll: boolean = false; - constructor( protected _resource: URI | null, private _id: string, - private _index: number, - private _query: ITextQuery, - private _parent: SearchResult, + protected _index: number | null, + protected _query: ITextQuery, + private _parent: SearchResult | FolderMatch, private _searchModel: SearchModel, + private _closestRoot: FolderMatchWorkspaceRoot | null, @IReplaceService private readonly replaceService: IReplaceService, - @IInstantiationService private readonly instantiationService: IInstantiationService, + @IInstantiationService protected readonly instantiationService: IInstantiationService, @ILabelService private readonly labelService: ILabelService, + @IUriIdentityService protected readonly uriIdentityService: IUriIdentityService ) { super(); this._fileMatches = new ResourceMap(); + this._folderMatches = new ResourceMap(); + this._folderMatchesMap = TernarySearchTree.forUris(key => this.uriIdentityService.extUri.ignorePathCasing(key)); this._unDisposedFileMatches = new ResourceMap(); + this._unDisposedFolderMatches = new ResourceMap(); } get searchModel(): SearchModel { @@ -509,6 +521,10 @@ export class FolderMatch extends Disposable { return this._parent.showHighlights; } + get closestRoot(): FolderMatchWorkspaceRoot | null { + return this._closestRoot; + } + set replacingAll(b: boolean) { this._replacingAll = b; } @@ -521,7 +537,7 @@ export class FolderMatch extends Disposable { return this._resource; } - index(): number { + index(): number | null { return this._index; } @@ -529,20 +545,121 @@ export class FolderMatch extends Disposable { return this.resource ? this.labelService.getUriBasenameLabel(this.resource) : ''; } - parent(): SearchResult { + parent(): SearchResult | FolderMatch { return this._parent; } bindModel(model: ITextModel): void { const fileMatch = this._fileMatches.get(model.uri); - fileMatch?.bindModel(model); + + if (fileMatch) { + fileMatch.bindModel(model); + } else { + this.folderMatches().forEach(e => { + e.bindModel(model); + }); + } } - add(raw: IFileMatch[], silent: boolean): void { + public createIntermediateFolderMatch(resource: URI, id: string, index: number | null, query: ITextQuery, baseWorkspaceFolder: FolderMatchWorkspaceRoot): FolderMatchWithResource { + const folderMatch = this.instantiationService.createInstance(FolderMatchWithResource, resource, id, index, query, this, this._searchModel, baseWorkspaceFolder); + this.configureIntermediateMatch(folderMatch); + this.doAddFolder(folderMatch); + return folderMatch; + } + + public configureIntermediateMatch(folderMatch: FolderMatchWithResource) { + const disposable = folderMatch.onChange((event) => this.onFolderChange(folderMatch, event)); + folderMatch.onDispose(() => disposable.dispose()); + } + + clear(): void { + const changed: FileMatch[] = this.downstreamFileMatches(); + this.disposeMatches(); + this._onChange.fire({ elements: changed, removed: true, added: false }); + } + + remove(matches: FileMatch | FolderMatchWithResource | (FileMatch | FolderMatchWithResource)[]): void { + if (!Array.isArray(matches)) { + matches = [matches]; + } + const allMatches = getFileMatches(matches); + this.doRemoveFile(allMatches); + } + + replace(match: FileMatch): Promise { + return this.replaceService.replace([match]).then(() => { + this.doRemoveFile([match]); + }); + } + + replaceAll(): Promise { + const matches = this.matches(); + return this.batchReplace(matches); + } + + matches(): (FileMatch | FolderMatchWithResource)[] { + return [...this.fileMatches(), ...this.folderMatches()]; + } + + fileMatches(): FileMatch[] { + return [...this._fileMatches.values()]; + } + + folderMatches(): FolderMatchWithResource[] { + return [...this._folderMatches.values()]; + } + + isEmpty(): boolean { + return (this.fileCount() + this.folderCount()) === 0; + } + + hasFileUriDownstream(uri: URI): FileMatch | null { + const directChildFileMatch = this._fileMatches.get(uri); + if (directChildFileMatch) { + return directChildFileMatch; + } + for (let i = 0; i < this.folderMatches().length; i++) { + const match = this.folderMatches()[i].hasFileUriDownstream(uri); + if (match) { + return match; + } + } + return null; + } + + downstreamFileMatches(): FileMatch[] { + const recursiveChildren = this.folderMatches().map(e => e.downstreamFileMatches()).flat(); + return [...this.fileMatches(), ...recursiveChildren]; + } + + private fileCount(): number { + return this._fileMatches.size; + } + + private folderCount(): number { + return this._folderMatches.size; + } + + count(): number { + return this.fileCount() + this.folderCount(); + } + + recursiveFileCount(): number { + return this.downstreamFileMatches().length; + } + + get query(): ITextQuery | null { + return this._query; + } + + addFileMatch(raw: IFileMatch[], silent: boolean): void { + // when adding a fileMatch that has intermediate directories const added: FileMatch[] = []; const updated: FileMatch[] = []; + raw.forEach(rawFileMatch => { - const existingFileMatch = this._fileMatches.get(rawFileMatch.resource); + const existingFileMatch = this.hasFileUriDownstream(rawFileMatch.resource); if (existingFileMatch) { rawFileMatch .results! @@ -555,11 +672,10 @@ export class FolderMatch extends Disposable { existingFileMatch.addContext(rawFileMatch.results); } else { - const fileMatch = this.instantiationService.createInstance(FileMatch, this._query.contentPattern, this._query.previewOptions, this._query.maxResults, this, rawFileMatch); - this.doAdd(fileMatch); - added.push(fileMatch); - const disposable = fileMatch.onChange(({ didRemove }) => this.onFileChange(fileMatch, didRemove)); - fileMatch.onDispose(() => disposable.dispose()); + if (this instanceof FolderMatchWorkspaceRoot || this instanceof FolderMatchNoRoot) { + const fileMatch = this.createAndConfigureFileMatch(rawFileMatch); + added.push(fileMatch); + } } }); @@ -569,56 +685,63 @@ export class FolderMatch extends Disposable { } } - clear(): void { - const changed: FileMatch[] = this.matches(); - this.disposeMatches(); - this._onChange.fire({ elements: changed, removed: true }); + doAddFile(fileMatch: FileMatch): void { + this._fileMatches.set(fileMatch.resource, fileMatch); + if (this._unDisposedFileMatches.has(fileMatch.resource)) { + this._unDisposedFileMatches.delete(fileMatch.resource); + } } - remove(matches: FileMatch | FileMatch[]): void { - this.doRemove(matches); + protected uriHasParent(parent: URI, child: URI) { + return this.uriIdentityService.extUri.isEqualOrParent(child, parent) && !this.uriIdentityService.extUri.isEqual(child, parent); } - replace(match: FileMatch): Promise { - return this.replaceService.replace([match]).then(() => { - this.doRemove(match); - }); + private isInParentChain(folderMatch: FolderMatchWithResource) { + + let matchItem: FolderMatch | SearchResult = this; + while (matchItem instanceof FolderMatch) { + if (matchItem.id() === folderMatch.id()) { + return true; + } + matchItem = matchItem.parent(); + } + return false; } - replaceAll(): Promise { - const matches = this.matches(); - return this.batchReplace(matches); + private getFolderMatch(resource: URI): FolderMatchWithResource | undefined { + const folderMatch = this._folderMatchesMap.findSubstr(resource); + return folderMatch; } - matches(): FileMatch[] { - return [...this._fileMatches.values()]; + doAddFolder(folderMatch: FolderMatchWithResource) { + if (this instanceof FolderMatchWithResource && !this.uriHasParent(this.resource, folderMatch.resource)) { + throw Error(`${folderMatch.resource} does not belong as a child of ${this.resource}`); + } else if (this.isInParentChain(folderMatch)) { + throw Error(`${folderMatch.resource} is a parent of ${this.resource}`); + } + + this._folderMatches.set(folderMatch.resource, folderMatch); + this._folderMatchesMap.set(folderMatch.resource, folderMatch); + if (this._unDisposedFolderMatches.has(folderMatch.resource)) { + this._unDisposedFolderMatches.delete(folderMatch.resource); + } } - isEmpty(): boolean { - return this.fileCount() === 0; + private async batchReplace(matches: (FileMatch | FolderMatchWithResource)[]): Promise { + const allMatches = getFileMatches(matches); + + await this.replaceService.replace(allMatches); + this.doRemoveFile(allMatches, true, true); } - fileCount(): number { - return this._fileMatches.size; - } - - count(): number { - return this.matches().reduce((prev, match) => prev + match.count(), 0); - } - - private async batchReplace(matches: FileMatch[]): Promise { - await this.replaceService.replace(matches); - this.doRemove(matches, true, true); - } - - private onFileChange(fileMatch: FileMatch, removed = false): void { + public onFileChange(fileMatch: FileMatch, removed = false): void { let added = false; if (!this._fileMatches.has(fileMatch.resource)) { - this.doAdd(fileMatch); + this.doAddFile(fileMatch); added = true; } if (fileMatch.count() === 0) { - this.doRemove(fileMatch, false, false); + this.doRemoveFile([fileMatch], false, false); added = false; removed = true; } @@ -627,37 +750,54 @@ export class FolderMatch extends Disposable { } } - private doAdd(fileMatch: FileMatch): void { - this._fileMatches.set(fileMatch.resource, fileMatch); - if (this._unDisposedFileMatches.has(fileMatch.resource)) { - this._unDisposedFileMatches.delete(fileMatch.resource); + public onFolderChange(folderMatch: FolderMatchWithResource, event: IChangeEvent): void { + if (!this._folderMatches.has(folderMatch.resource)) { + this.doAddFolder(folderMatch); } + if (folderMatch.isEmpty()) { + this._folderMatches.delete(folderMatch.resource); + folderMatch.dispose(); + } + + this._onChange.fire(event); } - private doRemove(fileMatches: FileMatch | FileMatch[], dispose: boolean = true, trigger: boolean = true): void { - if (!Array.isArray(fileMatches)) { - fileMatches = [fileMatches]; - } + private doRemoveFile(fileMatches: FileMatch[], dispose: boolean = true, trigger: boolean = true): void { + const removed = []; for (const match of fileMatches as FileMatch[]) { - this._fileMatches.delete(match.resource); - if (dispose) { - match.dispose(); + if (this.fileMatches().includes(match)) { + this._fileMatches.delete(match.resource); + if (dispose) { + match.dispose(); + } else { + this._unDisposedFileMatches.set(match.resource, match); + } + removed.push(match); } else { - this._unDisposedFileMatches.set(match.resource, match); + const folder = this.getFolderMatch(match.resource); + if (folder) { + folder.doRemoveFile([match], dispose, trigger); + } else { + throw Error(`FileMatch ${match.resource} is not located within FolderMatch ${this.resource}`); + } } } if (trigger) { - this._onChange.fire({ elements: fileMatches, removed: true }); + this._onChange.fire({ elements: removed, removed: true }); } } private disposeMatches(): void { [...this._fileMatches.values()].forEach((fileMatch: FileMatch) => fileMatch.dispose()); + [...this._folderMatches.values()].forEach((folderMatch: FolderMatch) => folderMatch.disposeMatches()); [...this._unDisposedFileMatches.values()].forEach((fileMatch: FileMatch) => fileMatch.dispose()); + [...this._unDisposedFolderMatches.values()].forEach((folderMatch: FolderMatch) => folderMatch.disposeMatches()); this._fileMatches.clear(); + this._folderMatches.clear(); this._unDisposedFileMatches.clear(); + this._unDisposedFolderMatches.clear(); } override dispose(): void { @@ -667,17 +807,15 @@ export class FolderMatch extends Disposable { } } -/** - * BaseFolderMatch => optional resource ("other files" node) - * FolderMatch => required resource (normal folder node) - */ export class FolderMatchWithResource extends FolderMatch { - constructor(_resource: URI, _id: string, _index: number, _query: ITextQuery, _parent: SearchResult, _searchModel: SearchModel, + + constructor(_resource: URI, _id: string, _index: number | null, _query: ITextQuery, _parent: SearchResult | FolderMatch, _searchModel: SearchModel, _closestRoot: FolderMatchWorkspaceRoot | null, @IReplaceService replaceService: IReplaceService, @IInstantiationService instantiationService: IInstantiationService, - @ILabelService labelService: ILabelService + @ILabelService labelService: ILabelService, + @IUriIdentityService uriIdentityService: IUriIdentityService ) { - super(_resource, _id, _index, _query, _parent, _searchModel, replaceService, instantiationService, labelService); + super(_resource, _id, _index, _query, _parent, _searchModel, _closestRoot, replaceService, instantiationService, labelService, uriIdentityService); } override get resource(): URI { @@ -685,13 +823,105 @@ export class FolderMatchWithResource extends FolderMatch { } } +/** + * FolderMatchWorkspaceRoot => folder for workspace root + */ +export class FolderMatchWorkspaceRoot extends FolderMatchWithResource { + constructor(_resource: URI, _id: string, _index: number, _query: ITextQuery, _parent: SearchResult, _searchModel: SearchModel, + @IReplaceService replaceService: IReplaceService, + @IInstantiationService instantiationService: IInstantiationService, + @ILabelService labelService: ILabelService, + @IUriIdentityService uriIdentityService: IUriIdentityService + ) { + super(_resource, _id, _index, _query, _parent, _searchModel, null, replaceService, instantiationService, labelService, uriIdentityService); + } + + private uriParent(uri: URI): URI { + return this.uriIdentityService.extUri.dirname(uri); + } + + private uriEquals(uri1: URI, ur2: URI): boolean { + return this.uriIdentityService.extUri.isEqual(uri1, ur2); + } + + private createFileMatch(query: IPatternInfo, previewOptions: ITextSearchPreviewOptions | undefined, maxResults: number | undefined, parent: FolderMatch, rawFileMatch: IFileMatch, closestRoot: FolderMatchWorkspaceRoot | null,): FileMatch { + const fileMatch = this.instantiationService.createInstance(FileMatch, query, previewOptions, maxResults, parent, rawFileMatch, closestRoot); + parent.doAddFile(fileMatch); + const disposable = fileMatch.onChange(({ didRemove }) => parent.onFileChange(fileMatch, didRemove)); + fileMatch.onDispose(() => disposable.dispose()); + return fileMatch; + } + + createAndConfigureFileMatch(rawFileMatch: IFileMatch): FileMatch { + + if (!this.uriHasParent(this.resource, rawFileMatch.resource)) { + throw Error(`${rawFileMatch.resource} is not a descendant of ${this.resource}`); + } + + const fileMatchParentParts: URI[] = []; + let uri = this.uriParent(rawFileMatch.resource); + + while (!this.uriEquals(this.resource, uri)) { + fileMatchParentParts.unshift(uri); + const prevUri = uri; + uri = this.uriParent(uri); + if (this.uriEquals(prevUri, uri)) { + throw Error(`${rawFileMatch.resource} is not correctly configured as a child of its ${this.resource}`); + } + } + + const root = this.closestRoot ?? this; + let parent: FolderMatch = this; + for (let i = 0; i < fileMatchParentParts.length; i++) { + let folderMatch: FolderMatchWithResource | undefined = parent.folderMatches().find(e => e.resource && (this.uriEquals(e.resource, fileMatchParentParts[i]))); + if (!folderMatch) { + folderMatch = parent.createIntermediateFolderMatch(fileMatchParentParts[i], fileMatchParentParts[i].toString(), null, this._query, root); + } + parent = folderMatch; + } + + return this.createFileMatch(this._query.contentPattern, this._query.previewOptions, this._query.maxResults, parent, rawFileMatch, root); + } + + override index(): number { + return this._index!; + } +} + +/** + * BaseFolderMatch => optional resource ("other files" node) + * FolderMatch => required resource (normal folder node) + */ +export class FolderMatchNoRoot extends FolderMatch { + constructor(_id: string, _index: number, _query: ITextQuery, _parent: SearchResult | FolderMatch, _searchModel: SearchModel, + @IReplaceService replaceService: IReplaceService, + @IInstantiationService instantiationService: IInstantiationService, + @ILabelService labelService: ILabelService, + @IUriIdentityService uriIdentityService: IUriIdentityService + ) { + super(null, _id, _index, _query, _parent, _searchModel, null, replaceService, instantiationService, labelService, uriIdentityService); + } + + createAndConfigureFileMatch(rawFileMatch: IFileMatch): FileMatch { + const fileMatch = this.instantiationService.createInstance(FileMatch, this._query.contentPattern, this._query.previewOptions, this._query.maxResults, this, rawFileMatch, null); + this.doAddFile(fileMatch); + const disposable = fileMatch.onChange(({ didRemove }) => this.onFileChange(fileMatch, didRemove)); + fileMatch.onDispose(() => disposable.dispose()); + return fileMatch; + } +} + /** * Compares instances of the same match type. Different match types should not be siblings * and their sort order is undefined. */ export function searchMatchComparer(elementA: RenderableMatch, elementB: RenderableMatch, sortOrder: SearchSortOrder = SearchSortOrder.Default): number { if (elementA instanceof FolderMatch && elementB instanceof FolderMatch) { - return elementA.index() - elementB.index(); + const elemAIndex = elementA.index(); + const elemBIndex = elementB.index(); + if (elemAIndex !== null && elemBIndex !== null) { + return elemAIndex - elemBIndex; + } } if (elementA instanceof FileMatch && elementB instanceof FileMatch) { @@ -705,10 +935,12 @@ export function searchMatchComparer(elementA: RenderableMatch, elementB: Rendera case SearchSortOrder.FileNames: return compareFileNames(elementA.name(), elementB.name()); case SearchSortOrder.Modified: { - const fileStatA = elementA.fileStat; - const fileStatB = elementB.fileStat; - if (fileStatA && fileStatB) { - return fileStatB.mtime - fileStatA.mtime; + if (!(elementA instanceof FolderMatch) || !(elementB instanceof FolderMatch)) { + const fileStatA = elementA.fileStat; + const fileStatB = elementB.fileStat; + if (fileStatA && fileStatB) { + return fileStatB.mtime - fileStatA.mtime; + } } } // Fall through otherwise @@ -765,9 +997,9 @@ export class SearchResult extends Disposable { merge: this.mergeEvents })); readonly onChange: Event = this._onChange.event; - private _folderMatches: FolderMatchWithResource[] = []; + private _folderMatches: FolderMatchWorkspaceRoot[] = []; private _otherFilesMatch: FolderMatch | null = null; - private _folderMatchesMap: TernarySearchTree = TernarySearchTree.forUris(key => this.uriIdentityService.extUri.ignorePathCasing(key)); + private _folderMatchesMap: TernarySearchTree = TernarySearchTree.forUris(key => this.uriIdentityService.extUri.ignorePathCasing(key)); private _showHighlights: boolean = false; private _query: ITextQuery | null = null; @@ -855,10 +1087,10 @@ export class SearchResult extends Disposable { this._folderMatches = (query && query.folderQueries || []) .map(fq => fq.folder) - .map((resource, index) => this.createFolderMatchWithResource(resource, resource.toString(), index, query)); + .map((resource, index) => this._createBaseFolderMatch(resource, resource.toString(), index, query)); this._folderMatches.forEach(fm => this._folderMatchesMap.set(fm.resource, fm)); - this._otherFilesMatch = this.createOtherFilesFolderMatch('otherFiles', this._folderMatches.length + 1, query); + this._otherFilesMatch = this._createBaseFolderMatch(null, 'otherFiles', this._folderMatches.length + 1, query); this._query = query; } @@ -888,16 +1120,13 @@ export class SearchResult extends Disposable { folderMatch?.bindModel(model); } - private createFolderMatchWithResource(resource: URI, id: string, index: number, query: ITextQuery): FolderMatchWithResource { - return this._createBaseFolderMatch(FolderMatchWithResource, resource, id, index, query); - } - - private createOtherFilesFolderMatch(id: string, index: number, query: ITextQuery): FolderMatch { - return this._createBaseFolderMatch(FolderMatch, null, id, index, query); - } - - private _createBaseFolderMatch(folderMatchClass: typeof FolderMatch | typeof FolderMatchWithResource, resource: URI | null, id: string, index: number, query: ITextQuery): FolderMatch { - const folderMatch = this.instantiationService.createInstance(folderMatchClass, resource, id, index, query, this, this._searchModel); + private _createBaseFolderMatch(resource: URI | null, id: string, index: number, query: ITextQuery): FolderMatch { + let folderMatch; + if (resource) { + folderMatch = this.instantiationService.createInstance(FolderMatchWorkspaceRoot, resource, id, index, query, this, this._searchModel); + } else { + folderMatch = this.instantiationService.createInstance(FolderMatchNoRoot, id, index, query, this, this._searchModel); + } const disposable = folderMatch.onChange((event) => this._onChange.fire(event)); folderMatch.onDispose(() => disposable.dispose()); return folderMatch; @@ -917,10 +1146,10 @@ export class SearchResult extends Disposable { } const folderMatch = this.getFolderMatch(raw[0].resource); - folderMatch?.add(raw, silent); + folderMatch?.addFileMatch(raw, silent); }); - this._otherFilesMatch?.add(other, silent); + this._otherFilesMatch?.addFileMatch(other, silent); this.disposePastResults(); } @@ -989,7 +1218,7 @@ export class SearchResult extends Disposable { matches(): FileMatch[] { const matches: FileMatch[][] = []; this.folderMatches().forEach(folderMatch => { - matches.push(folderMatch.matches()); + matches.push(folderMatch.downstreamFileMatches()); }); return ([]).concat(...matches); @@ -1000,7 +1229,7 @@ export class SearchResult extends Disposable { } fileCount(): number { - return this.folderMatches().reduce((prev, match) => prev + match.fileCount(), 0); + return this.folderMatches().reduce((prev, match) => prev + match.recursiveFileCount(), 0); } count(): number { @@ -1038,7 +1267,7 @@ export class SearchResult extends Disposable { return this._rangeHighlightDecorations; } - private getFolderMatch(resource: URI): FolderMatch { + private getFolderMatch(resource: URI): FolderMatchWorkspaceRoot | FolderMatch { const folderMatch = this._folderMatchesMap.findSubstr(resource); return folderMatch ? folderMatch : this._otherFilesMatch!; } @@ -1428,3 +1657,18 @@ export function arrayContainsElementOrParent(element: RenderableMatch, testArray return false; } + +function getFileMatches(matches: (FileMatch | FolderMatchWithResource)[]): FileMatch[] { + + const folderMatches: FolderMatchWithResource[] = []; + const fileMatches: FileMatch[] = []; + matches.forEach((e) => { + if (e instanceof FileMatch) { + fileMatches.push(e); + } else { + folderMatches.push(e); + } + }); + + return fileMatches.concat(folderMatches.map(e => e.downstreamFileMatches()).flat()); +} diff --git a/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts b/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts index d63211bccd4..623a2b6f972 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts @@ -43,7 +43,7 @@ suite('Search Actions', () => { const target = data[2]; const testObject: ReplaceAction = instantiationService.createInstance(ReplaceAction, tree, target, null); - const actual = testObject.getElementToFocusAfterRemoved(tree, target); + const actual = testObject.getElementToFocusAfterRemoved(tree, target, false); assert.strictEqual(data[4], actual); }); @@ -55,7 +55,7 @@ suite('Search Actions', () => { const target = data[5]; const testObject: ReplaceAction = instantiationService.createInstance(ReplaceAction, tree, target, null); - const actual = testObject.getElementToFocusAfterRemoved(tree, target); + const actual = testObject.getElementToFocusAfterRemoved(tree, target, false); assert.strictEqual(data[4], actual); }); @@ -67,7 +67,7 @@ suite('Search Actions', () => { const target = data[4]; const testObject: ReplaceAction = instantiationService.createInstance(ReplaceAction, tree, target, null); - const actual = testObject.getElementToFocusAfterRemoved(tree, target); + const actual = testObject.getElementToFocusAfterRemoved(tree, target, false); assert.strictEqual(data[2], actual); }); @@ -78,7 +78,7 @@ suite('Search Actions', () => { const target = data[1]; const testObject: ReplaceAction = instantiationService.createInstance(ReplaceAction, tree, target, null); - const actual = testObject.getElementToFocusAfterRemoved(tree, target); + const actual = testObject.getElementToFocusAfterRemoved(tree, target, false); assert.strictEqual(undefined, actual); }); @@ -91,7 +91,7 @@ suite('Search Actions', () => { const target = data[2]; const testObject: ReplaceAction = instantiationService.createInstance(ReplaceAction, tree, target, null); - const actual = testObject.getElementToFocusAfterRemoved(tree, target); + const actual = testObject.getElementToFocusAfterRemoved(tree, target, false); assert.strictEqual(data[4], actual); }); @@ -104,7 +104,7 @@ suite('Search Actions', () => { const target = data[4]; const testObject: ReplaceAction = instantiationService.createInstance(ReplaceAction, tree, target, null); - const actual = testObject.getElementToFocusAfterRemoved(tree, target); + const actual = testObject.getElementToFocusAfterRemoved(tree, target, false); assert.strictEqual(data[3], actual); }); @@ -115,7 +115,7 @@ suite('Search Actions', () => { const target = data[0]; const testObject: ReplaceAction = instantiationService.createInstance(ReplaceAction, tree, target, null); - const actual = testObject.getElementToFocusAfterRemoved(tree, target); + const actual = testObject.getElementToFocusAfterRemoved(tree, target, false); assert.strictEqual(undefined, actual); }); diff --git a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts index 28cf97e0b77..41a1754d7c0 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import { isWindows } from 'vs/base/common/platform'; -import { URI, URI as uri } from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { IModelService } from 'vs/editor/common/services/model'; import { ModelService } from 'vs/editor/common/services/modelService'; @@ -44,12 +44,12 @@ suite('Search - Viewlet', () => { type: QueryType.Text, contentPattern: { pattern: 'foo' }, folderQueries: [{ - folder: uri.parse('file://c:/') + folder: createFileUriFromPathFromRoot() }] }; result.add([{ - resource: uri.parse('file:///c:/foo'), + resource: createFileUriFromPathFromRoot('/foo'), results: [{ preview: { text: 'bar', @@ -72,14 +72,14 @@ suite('Search - Viewlet', () => { const fileMatch = result.matches()[0]; const lineMatch = fileMatch.matches()[0]; - assert.strictEqual(fileMatch.id(), 'file:///c%3A/foo'); - assert.strictEqual(lineMatch.id(), 'file:///c%3A/foo>[2,1 -> 2,2]b'); + assert.strictEqual(fileMatch.id(), URI.file(`${getRootName()}/foo`).toString()); + assert.strictEqual(lineMatch.id(), `${URI.file(`${getRootName()}/foo`).toString()}>[2,1 -> 2,2]b`); }); test('Comparer', () => { - const fileMatch1 = aFileMatch(isWindows ? 'C:\\foo' : '/c/foo'); - const fileMatch2 = aFileMatch(isWindows ? 'C:\\with\\path' : '/c/with/path'); - const fileMatch3 = aFileMatch(isWindows ? 'C:\\with\\path\\foo' : '/c/with/path/foo'); + const fileMatch1 = aFileMatch('/foo'); + const fileMatch2 = aFileMatch('/with/path'); + const fileMatch3 = aFileMatch('/with/path/foo'); const lineMatch1 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1)); const lineMatch2 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); const lineMatch3 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); @@ -95,10 +95,10 @@ suite('Search - Viewlet', () => { }); test('Advanced Comparer', () => { - const fileMatch1 = aFileMatch(isWindows ? 'C:\\with\\path\\foo10' : '/c/with/path/foo10'); - const fileMatch2 = aFileMatch(isWindows ? 'C:\\with\\path2\\foo1' : '/c/with/path2/foo1'); - const fileMatch3 = aFileMatch(isWindows ? 'C:\\with\\path2\\bar.a' : '/c/with/path2/bar.a'); - const fileMatch4 = aFileMatch(isWindows ? 'C:\\with\\path2\\bar.b' : '/c/with/path2/bar.b'); + const fileMatch1 = aFileMatch('/with/path/foo10'); + const fileMatch2 = aFileMatch('/with/path2/foo1'); + const fileMatch3 = aFileMatch('/with/path/bar.a'); + const fileMatch4 = aFileMatch('/with/path/bar.b'); // By default, path < path2 assert(searchMatchComparer(fileMatch1, fileMatch2) < 0); @@ -111,12 +111,12 @@ suite('Search - Viewlet', () => { test('Cross-type Comparer', () => { const searchResult = aSearchResult(); - const folderMatch1 = aFolderMatch(isWindows ? 'C:\\voo' : '/c/voo', 0, searchResult); - const folderMatch2 = aFolderMatch(isWindows ? 'C:\\with' : '/c/with', 1, searchResult); + const folderMatch1 = aFolderMatch('/voo', 0, searchResult); + const folderMatch2 = aFolderMatch('/with', 1, searchResult); - const fileMatch1 = aFileMatch(isWindows ? 'C:\\voo\\foo.a' : '/c/voo/foo.a', folderMatch1); - const fileMatch2 = aFileMatch(isWindows ? 'C:\\with\\path.c' : '/c/with/path.c', folderMatch2); - const fileMatch3 = aFileMatch(isWindows ? 'C:\\with\\path\\bar.b' : '/c/with/path/bar.b', folderMatch2); + const fileMatch1 = aFileMatch('/voo/foo.a', folderMatch1); + const fileMatch2 = aFileMatch('/with/path.c', folderMatch2); + const fileMatch3 = aFileMatch('/with/path/bar.b', folderMatch2); const lineMatch1 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1)); const lineMatch2 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); @@ -165,7 +165,7 @@ suite('Search - Viewlet', () => { function aFileMatch(path: string, parentFolder?: FolderMatch, ...lineMatches: ITextSearchMatch[]): FileMatch { const rawMatch: IFileMatch = { - resource: uri.file(path), + resource: createFileUriFromPathFromRoot(path), results: lineMatches }; return instantiation.createInstance(FileMatch, null, null, null, parentFolder, rawMatch); @@ -173,12 +173,12 @@ suite('Search - Viewlet', () => { function aFolderMatch(path: string, index: number, parent?: SearchResult): FolderMatch { const searchModel = instantiation.createInstance(SearchModel); - return instantiation.createInstance(FolderMatch, uri.file(path), path, index, null, parent, searchModel); + return instantiation.createInstance(FolderMatch, createFileUriFromPathFromRoot(path), path, index, null, parent, searchModel, parent); } function aSearchResult(): SearchResult { const searchModel = instantiation.createInstance(SearchModel); - searchModel.searchResult.query = { type: 1, folderQueries: [{ folder: URI.parse('file://c:/') }] }; + searchModel.searchResult.query = { type: 1, folderQueries: [{ folder: createFileUriFromPathFromRoot() }] }; return searchModel.searchResult; } @@ -187,4 +187,25 @@ suite('Search - Viewlet', () => { instantiationService.stub(IThemeService, new TestThemeService()); return instantiationService.createInstance(ModelService); } + + function createFileUriFromPathFromRoot(path?: string): URI { + const rootName = getRootName(); + if (path) { + return URI.file(`${rootName}${path}`); + } else { + if (isWindows) { + return URI.file(`${rootName}/`); + } else { + return URI.file(rootName); + } + } + } + + function getRootName(): string { + if (isWindows) { + return 'c:'; + } else { + return ''; + } + } }); diff --git a/src/vs/workbench/contrib/search/test/common/searchModel.test.ts b/src/vs/workbench/contrib/search/test/common/searchModel.test.ts index cd49e55a241..b61cf09fd01 100644 --- a/src/vs/workbench/contrib/search/test/common/searchModel.test.ts +++ b/src/vs/workbench/contrib/search/test/common/searchModel.test.ts @@ -23,6 +23,7 @@ import { FileService } from 'vs/platform/files/common/fileService'; import { NullLogService } from 'vs/platform/log/common/log'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; +import { isWindows } from 'vs/base/common/platform'; const nullEvent = new class { id: number = -1; @@ -64,7 +65,7 @@ suite('SearchModel', () => { }; const folderQueries: IFolderQuery[] = [ - { folder: URI.parse('file://c:/') } + { folder: createFileUriFromPathFromRoot() } ]; setup(() => { @@ -126,10 +127,10 @@ suite('SearchModel', () => { test('Search Model: Search adds to results', async () => { const results = [ - aRawMatch('file://c:/1', + aRawMatch('/1', new TextSearchMatch('preview 1', new OneLineRange(1, 1, 4)), new TextSearchMatch('preview 1', new OneLineRange(1, 4, 11))), - aRawMatch('file://c:/2', new TextSearchMatch('preview 2', lineOneRange))]; + aRawMatch('/2', new TextSearchMatch('preview 2', lineOneRange))]; instantiationService.stub(ISearchService, searchServiceWithResults(results)); const testObject: SearchModel = instantiationService.createInstance(SearchModel); @@ -138,7 +139,7 @@ suite('SearchModel', () => { const actual = testObject.searchResult.matches(); assert.strictEqual(2, actual.length); - assert.strictEqual('file://c:/1', actual[0].resource.toString()); + assert.strictEqual(URI.file(`${getRootName()}/1`).toString(), actual[0].resource.toString()); let actuaMatches = actual[0].matches(); assert.strictEqual(2, actuaMatches.length); @@ -156,10 +157,10 @@ suite('SearchModel', () => { test('Search Model: Search reports telemetry on search completed', async () => { const target = instantiationService.spy(ITelemetryService, 'publicLog'); const results = [ - aRawMatch('file://c:/1', + aRawMatch('/1', new TextSearchMatch('preview 1', new OneLineRange(1, 1, 4)), new TextSearchMatch('preview 1', new OneLineRange(1, 4, 11))), - aRawMatch('file://c:/2', + aRawMatch('/2', new TextSearchMatch('preview 2', lineOneRange))]; instantiationService.stub(ISearchService, searchServiceWithResults(results)); @@ -198,7 +199,7 @@ suite('SearchModel', () => { instantiationService.stub(ITelemetryService, 'publicLog', target1); instantiationService.stub(ISearchService, searchServiceWithResults( - [aRawMatch('file://c:/1', new TextSearchMatch('some preview', lineOneRange))], + [aRawMatch('/1', new TextSearchMatch('some preview', lineOneRange))], { results: [], stats: testSearchStats, messages: [] })); const testObject = instantiationService.createInstance(SearchModel); @@ -260,10 +261,10 @@ suite('SearchModel', () => { test('Search Model: Search results are cleared during search', async () => { const results = [ - aRawMatch('file://c:/1', + aRawMatch('/1', new TextSearchMatch('preview 1', new OneLineRange(1, 1, 4)), new TextSearchMatch('preview 1', new OneLineRange(1, 4, 11))), - aRawMatch('file://c:/2', + aRawMatch('/2', new TextSearchMatch('preview 2', lineOneRange))]; instantiationService.stub(ISearchService, searchServiceWithResults(results)); const testObject: SearchModel = instantiationService.createInstance(SearchModel); @@ -290,7 +291,7 @@ suite('SearchModel', () => { test('getReplaceString returns proper replace string for regExpressions', async () => { const results = [ - aRawMatch('file://c:/1', + aRawMatch('/1', new TextSearchMatch('preview 1', new OneLineRange(1, 1, 4)), new TextSearchMatch('preview 1', new OneLineRange(1, 4, 11)))]; instantiationService.stub(ISearchService, searchServiceWithResults(results)); @@ -320,7 +321,28 @@ suite('SearchModel', () => { }); function aRawMatch(resource: string, ...results: ITextSearchMatch[]): IFileMatch { - return { resource: URI.parse(resource), results }; + return { resource: createFileUriFromPathFromRoot(resource), results }; + } + + function createFileUriFromPathFromRoot(path?: string): URI { + const rootName = getRootName(); + if (path) { + return URI.file(`${rootName}${path}`); + } else { + if (isWindows) { + return URI.file(`${rootName}/`); + } else { + return URI.file(rootName); + } + } + } + + function getRootName(): string { + if (isWindows) { + return 'c:'; + } else { + return ''; + } } function stub(arg1: any, arg2: any, arg3: any): sinon.SinonStub { diff --git a/src/vs/workbench/contrib/search/test/common/searchResult.test.ts b/src/vs/workbench/contrib/search/test/common/searchResult.test.ts index bfa20e7ee4e..15ed87adcce 100644 --- a/src/vs/workbench/contrib/search/test/common/searchResult.test.ts +++ b/src/vs/workbench/contrib/search/test/common/searchResult.test.ts @@ -24,6 +24,7 @@ import { FileService } from 'vs/platform/files/common/fileService'; import { NullLogService } from 'vs/platform/log/common/log'; import { ILabelService } from 'vs/platform/label/common/label'; import { MockLabelService } from 'vs/workbench/services/label/test/common/mockLabelService'; +import { isWindows } from 'vs/base/common/platform'; const lineOneRange = new OneLineRange(1, 0, 1); @@ -144,7 +145,7 @@ suite('SearchResult', () => { assert.strictEqual(null, testObject.getSelectedMatch()); }); - test('Alle Drei Zusammen', function () { + test('Match -> FileMatch -> SearchResult hierarchy exists', function () { const searchResult = instantiationService.createInstance(SearchResult, null); const fileMatch = aFileMatch('far/boo', searchResult); const lineMatch = new Match(fileMatch, ['foo bar'], new OneLineRange(0, 0, 3), new OneLineRange(1, 0, 3)); @@ -155,7 +156,7 @@ suite('SearchResult', () => { test('Adding a raw match will add a file match with line matches', function () { const testObject = aSearchResult(); - const target = [aRawMatch('file://c:/', + const target = [aRawMatch('/1', new TextSearchMatch('preview 1', new OneLineRange(1, 1, 4)), new TextSearchMatch('preview 1', new OneLineRange(1, 4, 11)), new TextSearchMatch('preview 2', lineOneRange))]; @@ -166,7 +167,7 @@ suite('SearchResult', () => { const actual = testObject.matches(); assert.strictEqual(1, actual.length); - assert.strictEqual('file://c:/', actual[0].resource.toString()); + assert.strictEqual(URI.file(`${getRootName()}/1`).toString(), actual[0].resource.toString()); const actuaMatches = actual[0].matches(); assert.strictEqual(3, actuaMatches.length); @@ -184,10 +185,10 @@ suite('SearchResult', () => { test('Adding multiple raw matches', function () { const testObject = aSearchResult(); const target = [ - aRawMatch('file://c:/1', + aRawMatch('/1', new TextSearchMatch('preview 1', new OneLineRange(1, 1, 4)), new TextSearchMatch('preview 1', new OneLineRange(1, 4, 11))), - aRawMatch('file://c:/2', + aRawMatch('/2', new TextSearchMatch('preview 2', lineOneRange))]; testObject.add(target); @@ -196,7 +197,7 @@ suite('SearchResult', () => { const actual = testObject.matches(); assert.strictEqual(2, actual.length); - assert.strictEqual('file://c:/1', actual[0].resource.toString()); + assert.strictEqual(URI.file(`${getRootName()}/1`).toString(), actual[0].resource.toString()); let actuaMatches = actual[0].matches(); assert.strictEqual(2, actuaMatches.length); @@ -217,9 +218,9 @@ suite('SearchResult', () => { const testObject = aSearchResult(); testObject.add([ - aRawMatch('file://c:/1', + aRawMatch('/1', new TextSearchMatch('preview 1', lineOneRange)), - aRawMatch('file://c:/2', + aRawMatch('/2', new TextSearchMatch('preview 2', lineOneRange))]); testObject.matches()[0].onDispose(target1); @@ -236,7 +237,7 @@ suite('SearchResult', () => { const target = sinon.spy(); const testObject = aSearchResult(); testObject.add([ - aRawMatch('file://c:/1', + aRawMatch('/1', new TextSearchMatch('preview 1', lineOneRange))]); const objectToRemove = testObject.matches()[0]; testObject.onChange(target); @@ -251,9 +252,9 @@ suite('SearchResult', () => { const target = sinon.spy(); const testObject = aSearchResult(); testObject.add([ - aRawMatch('file://c:/1', + aRawMatch('/1', new TextSearchMatch('preview 1', lineOneRange)), - aRawMatch('file://c:/2', + aRawMatch('/2', new TextSearchMatch('preview 2', lineOneRange))]); const arrayToRemove = testObject.matches(); testObject.onChange(target); @@ -268,7 +269,7 @@ suite('SearchResult', () => { const target = sinon.spy(); const testObject = aSearchResult(); testObject.add([ - aRawMatch('file://c:/1', + aRawMatch('/1', new TextSearchMatch('preview 1', lineOneRange))]); const objectToRemove = testObject.matches()[0]; testObject.onChange(target); @@ -282,7 +283,7 @@ suite('SearchResult', () => { test('Removing all line matches and adding back will add file back to result', function () { const testObject = aSearchResult(); testObject.add([ - aRawMatch('file://c:/1', + aRawMatch('/1', new TextSearchMatch('preview 1', lineOneRange))]); const target = testObject.matches()[0]; const matchToRemove = target.matches()[0]; @@ -300,7 +301,7 @@ suite('SearchResult', () => { instantiationService.stub(IReplaceService, 'replace', voidPromise); const testObject = aSearchResult(); testObject.add([ - aRawMatch('file://c:/1', + aRawMatch('/1', new TextSearchMatch('preview 1', lineOneRange))]); testObject.replace(testObject.matches()[0]); @@ -314,7 +315,7 @@ suite('SearchResult', () => { instantiationService.stub(IReplaceService, 'replace', voidPromise); const testObject = aSearchResult(); testObject.add([ - aRawMatch('file://c:/1', + aRawMatch('/1', new TextSearchMatch('preview 1', lineOneRange))]); testObject.onChange(target); const objectToRemove = testObject.matches()[0]; @@ -332,9 +333,9 @@ suite('SearchResult', () => { instantiationService.stubPromise(IReplaceService, 'replace', voidPromise); const testObject = aSearchResult(); testObject.add([ - aRawMatch('file://c:/1', + aRawMatch('/1', new TextSearchMatch('preview 1', lineOneRange)), - aRawMatch('file://c:/2', + aRawMatch('/2', new TextSearchMatch('preview 2', lineOneRange))]); testObject.replaceAll(null!); @@ -347,11 +348,11 @@ suite('SearchResult', () => { const testObject = getPopulatedSearchResult(); const folderMatch = testObject.folderMatches()[0]; - const fileMatch = testObject.folderMatches()[1].matches()[0]; - const match = testObject.folderMatches()[1].matches()[1].matches()[0]; + const fileMatch = testObject.folderMatches()[1].downstreamFileMatches()[0]; + const match = testObject.folderMatches()[1].downstreamFileMatches()[1].matches()[0]; const arrayToRemove = [folderMatch, fileMatch, match]; - const expectedArrayResult = folderMatch.matches().concat([fileMatch, match.parent()]); + const expectedArrayResult = folderMatch.downstreamFileMatches().concat([fileMatch, match.parent()]); testObject.onChange(target); testObject.batchRemove(arrayToRemove); @@ -375,10 +376,10 @@ suite('SearchResult', () => { const testObject = getPopulatedSearchResult(); const folderMatch = testObject.folderMatches()[0]; - const fileMatch = testObject.folderMatches()[1].matches()[0]; - const match = testObject.folderMatches()[1].matches()[1].matches()[0]; + const fileMatch = testObject.folderMatches()[1].downstreamFileMatches()[0]; + const match = testObject.folderMatches()[1].downstreamFileMatches()[1].matches()[0]; - const firstExpectedMatch = folderMatch.matches()[0]; + const firstExpectedMatch = folderMatch.downstreamFileMatches()[0]; const arrayToRemove = [folderMatch, fileMatch, match]; @@ -392,6 +393,79 @@ suite('SearchResult', () => { sinon.assert.calledWith(replaceSpy.thirdCall, match); }); + test('Creating a model with nested folders should create the correct structure', function () { + const testObject = getPopulatedSearchResultForTreeTesting(); + + const root0 = testObject.folderMatches()[0]; + const root1 = testObject.folderMatches()[1]; + const root2 = testObject.folderMatches()[2]; + const root3 = testObject.folderMatches()[3]; + + const root0DownstreamFiles = root0.downstreamFileMatches(); + assert.deepStrictEqual(root0DownstreamFiles, root0.fileMatches().concat(root0.folderMatches()[0].fileMatches())); + assert.deepStrictEqual(root0.folderMatches()[0].downstreamFileMatches(), root0.folderMatches()[0].fileMatches()); + assert.deepStrictEqual(root0.folderMatches()[0].fileMatches()[0].parent(), root0.folderMatches()[0]); + assert.deepStrictEqual(root0.folderMatches()[0].parent(), root0); + assert.deepStrictEqual(root0.folderMatches()[0].closestRoot, root0); + root0DownstreamFiles.forEach((e) => { + assert.deepStrictEqual(e.closestRoot, root0); + }); + + const root1DownstreamFiles = root1.downstreamFileMatches(); + assert.deepStrictEqual(root1.downstreamFileMatches(), root1.fileMatches().concat(root1.folderMatches()[0].fileMatches())); // excludes the matches from nested root + assert.deepStrictEqual(root1.folderMatches()[0].fileMatches()[0].parent(), root1.folderMatches()[0]); + root1DownstreamFiles.forEach((e) => { + assert.deepStrictEqual(e.closestRoot, root1); + }); + + const root2DownstreamFiles = root2.downstreamFileMatches(); + assert.deepStrictEqual(root2DownstreamFiles, root2.fileMatches()); + assert.deepStrictEqual(root2.fileMatches()[0].parent(), root2); + assert.deepStrictEqual(root2.fileMatches()[0].closestRoot, root2); + + + const root3DownstreamFiles = root3.downstreamFileMatches(); + const root3Level3Folder = root3.folderMatches()[0].folderMatches()[0]; + assert.deepStrictEqual(root3DownstreamFiles, [root3.fileMatches(), ...root3Level3Folder.folderMatches()[0].fileMatches(), ...root3Level3Folder.folderMatches()[1].fileMatches()].flat()); + assert.deepStrictEqual(root3Level3Folder.downstreamFileMatches(), root3.folderMatches()[0].downstreamFileMatches()); + + assert.deepStrictEqual(root3Level3Folder.folderMatches()[1].fileMatches()[0].parent(), root3Level3Folder.folderMatches()[1]); + assert.deepStrictEqual(root3Level3Folder.folderMatches()[1].parent(), root3Level3Folder); + assert.deepStrictEqual(root3Level3Folder.parent(), root3.folderMatches()[0]); + + root3DownstreamFiles.forEach((e) => { + assert.deepStrictEqual(e.closestRoot, root3); + }); + }); + + test('Removing an intermediate folder should call OnChange() on all downstream file matches', function () { + const target = sinon.spy(); + const testObject = getPopulatedSearchResultForTreeTesting(); + + const folderMatch = testObject.folderMatches()[3].folderMatches()[0].folderMatches()[0].folderMatches()[0]; + + const expectedArrayResult = folderMatch.downstreamFileMatches(); + + testObject.onChange(target); + testObject.remove(folderMatch); + assert.ok(target.calledOnce); + assert.deepStrictEqual([{ elements: expectedArrayResult, removed: true, added: false }], target.args[0]); + }); + + test('Replacing an intermediate folder should remove all downstream folders and file matches', async function () { + const target = sinon.spy(); + const testObject = getPopulatedSearchResultForTreeTesting(); + + const folderMatch = testObject.folderMatches()[3].folderMatches()[0]; + + const expectedArrayResult = folderMatch.downstreamFileMatches(); + + testObject.onChange(target); + await testObject.batchReplace([folderMatch]); + assert.deepStrictEqual([{ elements: expectedArrayResult, removed: true, added: false }], target.args[0]); + + }); + function aFileMatch(path: string, searchResult?: SearchResult, ...lineMatches: ITextSearchMatch[]): FileMatch { const rawMatch: IFileMatch = { resource: URI.file('/' + path), @@ -402,12 +476,33 @@ suite('SearchResult', () => { function aSearchResult(): SearchResult { const searchModel = instantiationService.createInstance(SearchModel); - searchModel.searchResult.query = { type: 1, folderQueries: [{ folder: URI.parse('file://c:/') }] }; + searchModel.searchResult.query = { type: 1, folderQueries: [{ folder: createFileUriFromPathFromRoot() }] }; return searchModel.searchResult; } + function createFileUriFromPathFromRoot(path?: string): URI { + const rootName = getRootName(); + if (path) { + return URI.file(`${rootName}${path}`); + } else { + if (isWindows) { + return URI.file(`${rootName}/`); + } else { + return URI.file(rootName); + } + } + } + + function getRootName(): string { + if (isWindows) { + return 'c:'; + } else { + return ''; + } + } + function aRawMatch(resource: string, ...results: ITextSearchMatch[]): IFileMatch { - return { resource: URI.parse(resource), results }; + return { resource: createFileUriFromPathFromRoot(resource), results }; } function stubModelService(instantiationService: TestInstantiationService): IModelService { @@ -423,20 +518,86 @@ suite('SearchResult', () => { type: QueryType.Text, contentPattern: { pattern: 'foo' }, folderQueries: [{ - folder: URI.parse('file://c:/voo') + folder: createFileUriFromPathFromRoot('/voo') }, - { folder: URI.parse('file://c:/with') }, + { folder: createFileUriFromPathFromRoot('/with') }, ] }; testObject.add([ - aRawMatch('file://c:/voo/foo.a', + aRawMatch('/voo/foo.a', new TextSearchMatch('preview 1', lineOneRange), new TextSearchMatch('preview 2', lineOneRange)), - aRawMatch('file://c:/with/path/bar.b', + aRawMatch('/with/path/bar.b', new TextSearchMatch('preview 3', lineOneRange)), - aRawMatch('file://c:/with/path.c', + aRawMatch('/with/path.c', new TextSearchMatch('preview 4', lineOneRange), new TextSearchMatch('preview 5', lineOneRange)), ]); return testObject; } + + function getPopulatedSearchResultForTreeTesting() { + const testObject = aSearchResult(); + + testObject.query = { + type: QueryType.Text, + contentPattern: { pattern: 'foo' }, + folderQueries: [{ + folder: createFileUriFromPathFromRoot('/voo') + }, + { + folder: createFileUriFromPathFromRoot('/with') + }, + { + folder: createFileUriFromPathFromRoot('/with/test') + }, + { + folder: createFileUriFromPathFromRoot('/eep') + }, + ] + }; + /*** + * file structure looks like: + * *voo/ + * |- foo.a + * |- beep + * |- foo.c + * |- boop.c + * *with/ + * |- path + * |- bar.b + * |- path.c + * |- *test/ + * |- woo.c + * eep/ + * |- bar + * |- goo + * |- foo + * |- here.txt + * |- ooo + * |- there.txt + * |- eyy.y + */ + + testObject.add([ + aRawMatch('/voo/foo.a', + new TextSearchMatch('preview 1', lineOneRange), new TextSearchMatch('preview 2', lineOneRange)), + aRawMatch('/voo/beep/foo.c', + new TextSearchMatch('preview 1', lineOneRange), new TextSearchMatch('preview 2', lineOneRange)), + aRawMatch('/voo/beep/boop.c', + new TextSearchMatch('preview 3', lineOneRange)), + aRawMatch('/with/path.c', + new TextSearchMatch('preview 4', lineOneRange), new TextSearchMatch('preview 5', lineOneRange)), + aRawMatch('/with/path/bar.b', + new TextSearchMatch('preview 3', lineOneRange)), + aRawMatch('/with/test/woo.c', + new TextSearchMatch('preview 3', lineOneRange)), + aRawMatch('/eep/bar/goo/foo/here.txt', + new TextSearchMatch('preview 6', lineOneRange), new TextSearchMatch('preview 7', lineOneRange)), + aRawMatch('/eep/bar/goo/ooo/there.txt', + new TextSearchMatch('preview 6', lineOneRange), new TextSearchMatch('preview 7', lineOneRange)), + aRawMatch('/eep/eyy.y', + new TextSearchMatch('preview 6', lineOneRange), new TextSearchMatch('preview 7', lineOneRange)) + ]); + return testObject; + } }); diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts index ddaccc8641b..d42493a7a8a 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts @@ -236,7 +236,7 @@ export const serializeSearchResultForEditor = flattenSearchResultSerializations( flatten( searchResult.folderMatches().sort(matchComparer) - .map(folderMatch => folderMatch.matches().sort(matchComparer) + .map(folderMatch => folderMatch.downstreamFileMatches().sort(matchComparer) .map(fileMatch => fileMatchToSearchResultFormat(fileMatch, labelFormatter))))); return { diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts index bb6042ce80a..c882247c15d 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { IViewportRange, IBufferRange, IBufferLine, IBuffer, IBufferCellPosition } from 'xterm'; +import type { IViewportRange, IBufferRange, IBufferLine, IBufferCellPosition, IBuffer } from 'xterm'; import { IRange } from 'vs/editor/common/core/range'; import { OperatingSystem } from 'vs/base/common/platform'; import { IPath, posix, win32 } from 'vs/base/common/path'; @@ -138,6 +138,7 @@ export function getXtermLineContent(buffer: IBuffer, lineStart: number, lineEnd: return content; } + export function positionIsInRange(position: IBufferCellPosition, range: IBufferRange): boolean { if (position.y < range.start.y || position.y > range.end.y) { return false; diff --git a/src/vs/workbench/contrib/terminal/browser/remotePty.ts b/src/vs/workbench/contrib/terminal/browser/remotePty.ts index 0a32dbab6ed..7b7ac69688f 100644 --- a/src/vs/workbench/contrib/terminal/browser/remotePty.ts +++ b/src/vs/workbench/contrib/terminal/browser/remotePty.ts @@ -107,6 +107,13 @@ export class RemotePty extends Disposable implements ITerminalChildProcess { }); } + freePortKillProcess(port: string): Promise<{ port: string; processId: string }> { + if (!this._remoteTerminalChannel.freePortKillProcess) { + throw new Error('freePortKillProcess does not exist on the local pty service'); + } + return this._remoteTerminalChannel.freePortKillProcess(this.id, port); + } + acknowledgeDataEvent(charCount: number): void { // Support flow control for server spawned processes if (this._inReplay) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index ed581516f85..8af479229ed 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -242,10 +242,6 @@ registerSendSequenceKeybinding(String.fromCharCode('A'.charCodeAt(0) - 64), { registerSendSequenceKeybinding(String.fromCharCode('E'.charCodeAt(0) - 64), { mac: { primary: KeyMod.CtrlCmd | KeyCode.RightArrow } }); -// Break: ctrl+C -registerSendSequenceKeybinding(String.fromCharCode('C'.charCodeAt(0) - 64), { - mac: { primary: KeyMod.CtrlCmd | KeyCode.Period } -}); // NUL: ctrl+shift+2 registerSendSequenceKeybinding('\u0000', { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Digit2, diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index ee8797508f0..b3900ffd571 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -4,7 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { Orientation } from 'vs/base/browser/ui/splitview/splitview'; +import { IAction } from 'vs/base/common/actions'; import { Event } from 'vs/base/common/event'; +import { Lazy } from 'vs/base/common/lazy'; import { IDisposable } from 'vs/base/common/lifecycle'; import { OperatingSystem } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; @@ -17,6 +19,7 @@ import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IEditableData } from 'vs/workbench/common/views'; import { TerminalFindWidget } from 'vs/workbench/contrib/terminal/browser/terminalFindWidget'; import { ITerminalStatusList } from 'vs/workbench/contrib/terminal/browser/terminalStatusList'; +import { IContextualAction } from 'vs/workbench/contrib/terminal/browser/xterm/contextualActionAddon'; import { INavigationMode, IRegisterContributedProfileArgs, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalBackend, ITerminalConfigHelper, ITerminalFont, ITerminalProcessExtHostProxy } from 'vs/workbench/contrib/terminal/common/terminal'; import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; import { IMarker } from 'xterm'; @@ -456,7 +459,9 @@ export interface ITerminalInstance { readonly statusList: ITerminalStatusList; - readonly findWidget: TerminalFindWidget; + contextualActions: IContextualAction | undefined; + + readonly findWidget: Lazy; /** * The process ID of the shell process, this is undefined when there is no process associated @@ -579,6 +584,8 @@ export interface ITerminalInstance { onDidChangeFindResults: Event<{ resultIndex: number; resultCount: number } | undefined>; + onDidFocusFindWidget: Event; + readonly exitCode: number | undefined; readonly exitReason: TerminalExitReason | undefined; @@ -903,6 +910,36 @@ export interface ITerminalInstance { * Activates the most recent link of the given type. */ openRecentLink(type: 'localFile' | 'url'): Promise; + + /** + * Registers contextual action listeners + */ + registerContextualActions(...options: ITerminalContextualActionOptions[]): void; + + freePortKillProcess(port: string): Promise; +} + +export interface ITerminalContextualActionOptions { + actionName: string | DynamicActionName; + commandLineMatcher: string | RegExp; + outputMatcher?: ITerminalOutputMatcher; + getActions: ContextualActionCallback; + exitStatus?: boolean; +} +export type ContextualMatchResult = { commandLineMatch: RegExpMatchArray; outputMatch?: RegExpMatchArray | null }; +export type DynamicActionName = (matchResult: ContextualMatchResult) => string; +export type ContextualActionCallback = (matchResult: ContextualMatchResult, command: ITerminalCommand) => ICommandAction[] | undefined; + +export interface ICommandAction extends IAction { + commandToRunInTerminal?: string; + addNewLine?: boolean; +} + +export interface ITerminalOutputMatcher { + lineMatcher: string | RegExp; + anchor?: 'top' | 'bottom'; + offset?: number; + length?: number; } export interface IXtermTerminal { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index f777a57eb3d..f0128ece068 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -143,6 +143,26 @@ export function registerTerminalActions() { } }); + registerAction2(class extends Action2 { + constructor() { + super({ + id: TerminalCommandId.QuickFix, + title: { value: localize('workbench.action.terminal.quickFix', "Quick Fix"), original: 'Quick Fix' }, + f1: true, + category, + precondition: TerminalContextKeys.processSupported, + keybinding: { + primary: KeyMod.CtrlCmd | KeyCode.Period, + when: TerminalContextKeys.focus, + weight: KeybindingWeight.WorkbenchContrib + }, + }); + } + async run(accessor: ServicesAccessor) { + accessor.get(ITerminalService).activeInstance?.contextualActions?.showQuickFixMenu(); + } + }); + // Register new with profile command refreshTerminalActions([]); @@ -350,7 +370,7 @@ export function registerTerminalActions() { return; } const output = command.getOutput(); - if (output) { + if (output && typeof output === 'string') { await accessor.get(IClipboardService).writeText(output); } } @@ -1071,7 +1091,7 @@ export function registerTerminalActions() { }); } run(accessor: ServicesAccessor) { - accessor.get(ITerminalService).activeInstance?.findWidget.reveal(); + accessor.get(ITerminalService).activeInstance?.findWidget.getValue().reveal(); } }); registerAction2(class extends Action2 { @@ -1091,7 +1111,7 @@ export function registerTerminalActions() { }); } run(accessor: ServicesAccessor) { - accessor.get(ITerminalService).activeInstance?.findWidget.hide(); + accessor.get(ITerminalService).activeInstance?.findWidget.getValue().hide(); } }); @@ -1309,7 +1329,7 @@ export function registerTerminalActions() { title: { value: localize('workbench.action.terminal.toggleEscapeSequenceLogging', "Toggle Escape Sequence Logging"), original: 'Toggle Escape Sequence Logging' }, f1: true, category, - precondition: TerminalContextKeys.processSupported + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) }); } async run(accessor: ServicesAccessor) { @@ -1444,7 +1464,7 @@ export function registerTerminalActions() { } run(accessor: ServicesAccessor) { const terminalService = accessor.get(ITerminalService); - const state = terminalService.activeInstance?.findWidget.findState; + const state = terminalService.activeInstance?.findWidget.getValue().findState; state?.change({ isRegex: !state.isRegex }, false); } }); @@ -1466,7 +1486,7 @@ export function registerTerminalActions() { } run(accessor: ServicesAccessor) { const terminalService = accessor.get(ITerminalService); - const state = terminalService.activeInstance?.findWidget.findState; + const state = terminalService.activeInstance?.findWidget.getValue().findState; state?.change({ wholeWord: !state.wholeWord }, false); } }); @@ -1488,7 +1508,7 @@ export function registerTerminalActions() { } run(accessor: ServicesAccessor) { const terminalService = accessor.get(ITerminalService); - const state = terminalService.activeInstance?.findWidget.findState; + const state = terminalService.activeInstance?.findWidget.getValue().findState; state?.change({ matchCase: !state.matchCase }, false); } }); @@ -1517,8 +1537,11 @@ export function registerTerminalActions() { } run(accessor: ServicesAccessor) { const terminalService = accessor.get(ITerminalService); - terminalService.activeInstance?.findWidget.show(); - terminalService.activeInstance?.findWidget.find(false); + const findWidget = terminalService.activeInstance?.findWidget.getValue(); + if (findWidget) { + findWidget.show(); + findWidget.find(false); + } } }); registerAction2(class extends Action2 { @@ -1546,8 +1569,11 @@ export function registerTerminalActions() { } run(accessor: ServicesAccessor) { const terminalService = accessor.get(ITerminalService); - terminalService.activeInstance?.findWidget.show(); - terminalService.activeInstance?.findWidget.find(true); + const findWidget = terminalService.activeInstance?.findWidget.getValue(); + if (findWidget) { + findWidget.show(); + findWidget.find(true); + } } }); registerAction2(class extends Action2 { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalBaseContextualActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalBaseContextualActions.ts new file mode 100644 index 00000000000..8e2344fe159 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/terminalBaseContextualActions.ts @@ -0,0 +1,117 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IAction } from 'vs/base/common/actions'; +import { isWindows } from 'vs/base/common/platform'; +import { localize } from 'vs/nls'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { ContextualMatchResult, ICommandAction, ITerminalContextualActionOptions, ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalCommand } from 'vs/workbench/contrib/terminal/common/terminal'; + +export const GitCommandLineRegex = /git/; +export const GitPushCommandLineRegex = /git\s+push/; +export const AnyCommandLineRegex = /.{4,}/; +export const GitSimilarOutputRegex = /most similar command is\s+([^\s]{3,})/; +export const FreePortOutputRegex = /address already in use \d\.\d\.\d\.\d:(\d\d\d\d)\s+|Unable to bind [^ ]*:(\d+)|can't listen on port (\d+)|listen EADDRINUSE [^ ]*:(\d+)/; +export const GitPushOutputRegex = /git push --set-upstream origin ([^\s]+)\s+/; +export const GitCreatePrOutputRegex = /Create a pull request for \'([^\s]+)\' on GitHub by visiting:\s+remote:\s+(https:.+pull.+)\s+/; + +export function gitSimilarCommand(): ITerminalContextualActionOptions { + return { + commandLineMatcher: GitCommandLineRegex, + outputMatcher: { lineMatcher: GitSimilarOutputRegex, anchor: 'bottom' }, + actionName: (matchResult: ContextualMatchResult) => matchResult.outputMatch ? `Run git ${matchResult.outputMatch[1]}` : ``, + exitStatus: false, + getActions: (matchResult: ContextualMatchResult, command: ITerminalCommand) => { + const actions: ICommandAction[] = []; + const fixedCommand = matchResult?.outputMatch?.[1]; + if (!fixedCommand) { + return; + } + const label = localize("terminal.gitSimilarCommand", "Run git {0}", fixedCommand); + actions.push({ + class: undefined, tooltip: label, id: 'terminal.gitSimilarCommand', label, enabled: true, + commandToRunInTerminal: `git ${fixedCommand}`, + addNewLine: true, + run: () => { } + }); + return actions; + } + }; +} +export function freePort(terminalInstance?: Partial): ITerminalContextualActionOptions { + return { + actionName: (matchResult: ContextualMatchResult) => matchResult.outputMatch ? `Free port ${matchResult.outputMatch[1]}` : '', + commandLineMatcher: AnyCommandLineRegex, + outputMatcher: !isWindows ? { lineMatcher: FreePortOutputRegex, anchor: 'bottom' } : undefined, + getActions: (matchResult: ContextualMatchResult, command: ITerminalCommand) => { + const port = matchResult?.outputMatch?.[1]; + if (!port) { + return; + } + const actions: ICommandAction[] = []; + const label = localize("terminal.freePort", "Free port {0}", port); + actions.push({ + class: undefined, tooltip: label, id: 'terminal.freePort', label, enabled: true, + run: async () => { + await terminalInstance?.freePortKillProcess?.(port); + }, + commandToRunInTerminal: command.command, + addNewLine: false + }); + return actions; + } + }; +} +export function gitPushSetUpstream(): ITerminalContextualActionOptions { + return { + actionName: (matchResult: ContextualMatchResult) => matchResult.outputMatch ? `Git push ${matchResult.outputMatch[1]}` : '', + commandLineMatcher: GitPushCommandLineRegex, + outputMatcher: { lineMatcher: GitPushOutputRegex, anchor: 'bottom' }, + exitStatus: false, + getActions: (matchResult: ContextualMatchResult, command: ITerminalCommand) => { + const branch = matchResult?.outputMatch?.[1]; + if (!branch) { + return; + } + const actions: ICommandAction[] = []; + const label = localize("terminal.gitPush", "Git push {0}", branch); + command.command = `git push --set-upstream origin ${branch}`; + actions.push({ + class: undefined, tooltip: label, id: 'terminal.gitPush', label, enabled: true, + commandToRunInTerminal: command.command, + addNewLine: true, + run: () => { } + }); + return actions; + } + }; +} + +export function gitCreatePr(openerService: IOpenerService): ITerminalContextualActionOptions { + return { + actionName: (matchResult: ContextualMatchResult) => matchResult.outputMatch ? `Create PR for ${matchResult.outputMatch[1]}` : '', + commandLineMatcher: GitPushCommandLineRegex, + outputMatcher: { lineMatcher: GitCreatePrOutputRegex, anchor: 'bottom' }, + exitStatus: true, + getActions: (matchResult: ContextualMatchResult, command?: ITerminalCommand) => { + if (!command) { + return; + } + const branch = matchResult?.outputMatch?.[1]; + const link = matchResult?.outputMatch?.[2]; + if (!branch || !link) { + return; + } + const actions: IAction[] = []; + const label = localize("terminal.gitCreatePr", "Create PR"); + actions.push({ + class: undefined, tooltip: label, id: 'terminal.gitCreatePr', label, enabled: true, + run: () => openerService.open(link) + }); + return actions; + } + }; +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminalContextMenu.ts b/src/vs/workbench/contrib/terminal/browser/terminalContextMenu.ts index da86f8db4ed..d847bcdff56 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalContextMenu.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalContextMenu.ts @@ -4,12 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { Action, IAction } from 'vs/base/common/actions'; +import { IAction } from 'vs/base/common/actions'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenu } from 'vs/platform/actions/common/actions'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -export function openContextMenu(event: MouseEvent, parent: HTMLElement, menu: IMenu, contextMenuService: IContextMenuService, extraActions?: Action[]): void { +export function openContextMenu(event: MouseEvent, parent: HTMLElement, menu: IMenu, contextMenuService: IContextMenuService, extraActions?: IAction[]): void { const standardEvent = new StandardMouseEvent(event); const anchor: { x: number; y: number } = { x: standardEvent.posx, y: standardEvent.posy }; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts index 40405849e84..b71eabb4d8b 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts @@ -79,7 +79,7 @@ export class TerminalEditor extends EditorPane { // panel and the editors, this is needed so that the active instance gets set // when focus changes between them. this._register(this._editorInput.terminalInstance.onDidFocus(() => this._setActiveInstance())); - this._register(this._editorInput.terminalInstance.findWidget.focusTracker.onDidFocus(() => this._setActiveInstance())); + this._register(this._editorInput.terminalInstance.onDidFocusFindWidget(() => this._setActiveInstance())); this._editorInput.setCopyLaunchConfig(this._editorInput.terminalInstance.shellLaunchConfig); } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts b/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts index 199d9e48483..98c5d13d273 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts @@ -369,7 +369,7 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { this._setActiveInstance(instance); this._onDidFocusInstance.fire(instance); }), - instance.findWidget.focusTracker.onDidFocus(() => this._setActiveInstance(instance)), + instance.onDidFocusFindWidget(() => this._setActiveInstance(instance)), instance.capabilities.onDidAddCapability(() => this._onDidChangeInstanceCapability.fire(instance)), instance.capabilities.onDidRemoveCapability(() => this._onDidChangeInstanceCapability.fire(instance)), ]); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts b/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts index 0a0f2d7200e..1e7b0333e87 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts @@ -174,6 +174,11 @@ export class TerminalGroupService extends Disposable implements ITerminalGroupSe await timeout(0); const instance = this.activeInstance; if (instance) { + // HACK: Ensure the panel is still visible at this point as there may have been + // a request since it was opened to show a different panel + if (pane && !pane.isVisible()) { + await this._viewsService.openView(TERMINAL_VIEW_ID, focus); + } await instance.focusWhenReady(true); // HACK: as a workaround for https://github.com/microsoft/vscode/issues/134692, // this will trigger a forced refresh of the viewport to sync the viewport and scroll bar. diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index c59a61ec633..c17ac12ef00 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -17,6 +17,7 @@ import { ErrorNoTelemetry } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; import { ISeparator, template } from 'vs/base/common/labels'; +import { Lazy } from 'vs/base/common/lazy'; import { Disposable, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import * as path from 'vs/base/common/path'; @@ -59,8 +60,9 @@ import { AudioCue, IAudioCueService } from 'vs/workbench/contrib/audioCues/brows import { TaskSettingId } from 'vs/workbench/contrib/tasks/common/tasks'; import { IDetectedLinks, TerminalLinkManager } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager'; import { TerminalLinkQuickpick } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkQuickpick'; -import { IRequestAddInstanceToGroupEvent, ITerminalExternalLinkProvider, ITerminalInstance, TerminalDataTransfers } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IRequestAddInstanceToGroupEvent, ITerminalContextualActionOptions, ITerminalExternalLinkProvider, ITerminalInstance, TerminalDataTransfers } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalLaunchHelpAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; +import { gitSimilarCommand, gitCreatePr, gitPushSetUpstream, freePort } from 'vs/workbench/contrib/terminal/browser/terminalBaseContextualActions'; import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput'; import { TerminalFindWidget } from 'vs/workbench/contrib/terminal/browser/terminalFindWidget'; @@ -72,6 +74,7 @@ import { TypeAheadAddon } from 'vs/workbench/contrib/terminal/browser/terminalTy import { getTerminalResourcesFromDragEvent, getTerminalUri } from 'vs/workbench/contrib/terminal/browser/terminalUri'; import { EnvironmentVariableInfoWidget } from 'vs/workbench/contrib/terminal/browser/widgets/environmentVariableInfoWidget'; import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; +import { ContextualActionAddon, IContextualAction } from 'vs/workbench/contrib/terminal/browser/xterm/contextualActionAddon'; import { LineDataEventAddon } from 'vs/workbench/contrib/terminal/browser/xterm/lineDataEventAddon'; import { NavigationModeAddon } from 'vs/workbench/contrib/terminal/browser/xterm/navigationModeAddon'; import { XtermTerminal } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal'; @@ -138,10 +141,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private readonly _scopedInstantiationService: IInstantiationService; - private readonly _processManager: ITerminalProcessManager; + readonly _processManager: ITerminalProcessManager; private readonly _resource: URI; private _shutdownPersistentProcessId: number | undefined; - // Enables disposal of the xterm onKey // event when the CwdDetection capability // is added @@ -205,11 +207,18 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private _target?: TerminalLocation | undefined; private _disableShellIntegrationReporting: boolean | undefined; private _usedShellIntegrationInjection: boolean = false; + private _contextualActionAddon: ContextualActionAddon | undefined; readonly capabilities = new TerminalCapabilityStoreMultiplexer(); readonly statusList: ITerminalStatusList; - readonly findWidget: TerminalFindWidget; + /** + * Enables opening the contextual actions, if any, that are available + * and registering of command finished listeners + */ + get contextualActions(): IContextualAction | undefined { return this._contextualActionAddon; } + + readonly findWidget: Lazy; xterm?: XtermTerminal; disableLayout: boolean = false; @@ -346,6 +355,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { readonly onDidChangeHasChildProcesses = this._onDidChangeHasChildProcesses.event; private readonly _onDidChangeFindResults = new Emitter<{ resultIndex: number; resultCount: number } | undefined>(); readonly onDidChangeFindResults = this._onDidChangeFindResults.event; + private readonly _onDidFocusFindWidget = new Emitter(); + readonly onDidFocusFindWidget = this._onDidFocusFindWidget.event; constructor( private readonly _terminalShellTypeContextKey: IContextKey, @@ -437,7 +448,18 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._terminalAltBufferActiveContextKey = TerminalContextKeys.altBufferActive.bindTo(scopedContextKeyService); this._terminalShellIntegrationEnabledContextKey = TerminalContextKeys.terminalShellIntegrationEnabled.bindTo(scopedContextKeyService); - this.findWidget = this._scopedInstantiationService.createInstance(TerminalFindWidget, new FindReplaceState(), this); + this.findWidget = new Lazy(() => { + const findWidget = this._scopedInstantiationService.createInstance(TerminalFindWidget, new FindReplaceState(), this); + this._register(findWidget.focusTracker.onDidFocus(() => { + this._container?.classList.toggle('find-focused', true); + this._onDidFocusFindWidget.fire(); + })); + this._register(findWidget.focusTracker.onDidBlur(() => this._container?.classList.toggle('find-focused', false))); + if (this._container) { + this._container.appendChild(findWidget.getDomNode()); + } + return findWidget; + }); this._logService.trace(`terminalInstance#ctor (instanceId: ${this.instanceId})`, this._shellLaunchConfig); this._register(this.capabilities.onDidAddCapability(e => { @@ -450,7 +472,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._scopedInstantiationService.invokeFunction(getDirectoryHistory)?.add(e, { remoteAuthority: this.remoteAuthority }); }); } else if (e === TerminalCapability.CommandDetection) { - this.capabilities.get(TerminalCapability.CommandDetection)?.onCommandFinished(e => { + const commandCapability = this.capabilities.get(TerminalCapability.CommandDetection); + commandCapability?.onCommandFinished(e => { if (e.command.trim().length > 0) { this._scopedInstantiationService.invokeFunction(getCommandHistory)?.add(e.command, { shellType: this._shellType }); } @@ -563,9 +586,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { window.clearTimeout(initialDataEventsTimeout); } })); - - this._register(this.findWidget.focusTracker.onDidFocus(() => this._container?.classList.toggle('find-focused', true))); - this._register(this.findWidget.focusTracker.onDidBlur(() => this._container?.classList.toggle('find-focused', false))); } private _getIcon(): TerminalIcon | undefined { @@ -590,6 +610,12 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { return undefined; } + registerContextualActions(...actionOptions: ITerminalContextualActionOptions[]): void { + for (const actionOption of actionOptions) { + this.contextualActions?.registerCommandFinishedListener(actionOption); + } + } + private _initDimensions(): void { // The terminal panel needs to have been created to get the real view dimensions if (!this._container) { @@ -702,6 +728,10 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { const xterm = this._scopedInstantiationService.createInstance(XtermTerminal, Terminal, this._configHelper, this._cols, this._rows, this.target || TerminalLocation.Panel, this.capabilities, this.disableShellIntegrationReporting); this.xterm = xterm; + this._contextualActionAddon = this._scopedInstantiationService.createInstance(ContextualActionAddon, this.capabilities); + this.xterm?.raw.loadAddon(this._contextualActionAddon); + this.registerContextualActions(gitSimilarCommand(), gitCreatePr(this._openerService), gitPushSetUpstream(), freePort(this)); + this._register(this._contextualActionAddon.onDidRequestRerunCommand((e) => this.sendText(e.command, e.addNewLine || false))); const lineDataEventAddon = new LineDataEventAddon(); this.xterm.raw.loadAddon(lineDataEventAddon); this.updateAccessibilitySupport(); @@ -709,7 +739,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { if (e.copyAsHtml) { this.copySelection(true, e.command); } else { - this.sendText(e.command.command, true); + this.sendText(e.command.command, e.noNewLine ? false : true); } }); // Write initial text, deferring onLineFeed listener when applicable to avoid firing @@ -861,7 +891,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // The container changed, reattach this._container = container; this._container.appendChild(this._wrapperElement); - this._container.appendChild(this.findWidget.getDomNode()); + if (this.findWidget.hasValue()) { + this._container.appendChild(this.findWidget.getValue().getDomNode()); + } setTimeout(() => this._initDragAndDrop(container)); } @@ -883,7 +915,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._wrapperElement.appendChild(xtermElement); this._container.appendChild(this._wrapperElement); - this._container.appendChild(this.findWidget.getDomNode()); + if (this.findWidget.hasValue()) { + this._container.appendChild(this.findWidget.getValue().getDomNode()); + } const xterm = this.xterm; @@ -892,7 +926,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { const screenElement = xterm.attachToElement(xtermElement); - this._register(xterm.onDidChangeFindResults(() => this.findWidget.updateResultCount())); + this._register(xterm.onDidChangeFindResults(() => this.findWidget.getValue().updateResultCount())); this._register(xterm.shellIntegration.onDidChangeStatus(() => { if (this.hasFocus) { this._setShellIntegrationContextKey(); @@ -1163,7 +1197,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { dispose(this._linkManager); this._linkManager = undefined; dispose(this._widgetManager); - dispose(this.findWidget); + dispose(this.findWidget.rawValue); if (this.xterm?.raw.element) { this._hadFocusOnExit = this.hasFocus; @@ -1483,6 +1517,10 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this.xterm?.markTracker.scrollToClosestMarker(startMarkId, endMarkId, highlight); } + public async freePortKillProcess(port: string): Promise { + await this._processManager?.freePortKillProcess(port); + } + private _onProcessData(ev: IProcessDataEvent): void { const messageId = ++this._latestXtermWriteData; if (ev.trackCommit) { @@ -1814,7 +1852,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } this._resize(); - this.findWidget.layout(dimension.width); + this.findWidget.rawValue?.layout(dimension.width); // Signal the container is ready this._containerReadyBarrier.open(); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index c1c0ddf9f50..08da73cc57b 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -36,6 +36,8 @@ import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { TaskSettingId } from 'vs/workbench/contrib/tasks/common/tasks'; +import Severity from 'vs/base/common/severity'; +import { INotificationService } from 'vs/platform/notification/common/notification'; /** The amount of time to consider terminal errors to be related to the launch */ const LAUNCHING_DURATION = 500; @@ -136,7 +138,8 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce @ITerminalProfileResolverService private readonly _terminalProfileResolverService: ITerminalProfileResolverService, @IConfigurationService private readonly _configurationService: IConfigurationService, @ITerminalInstanceService private readonly _terminalInstanceService: ITerminalInstanceService, - @ITelemetryService private readonly _telemetryService: ITelemetryService + @ITelemetryService private readonly _telemetryService: ITelemetryService, + @INotificationService private readonly _notificationService: INotificationService ) { super(); @@ -171,6 +174,17 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce } } + async freePortKillProcess(port: string): Promise { + try { + if (this._process?.freePortKillProcess) { + const result = await this._process?.freePortKillProcess(port); + this._notificationService.notify({ message: `Killed process w ID: ${result.processId} to free port ${result.port}`, severity: Severity.Info }); + } + } catch (e) { + this._notificationService.notify({ message: `Could not kill process for port ${port} wth error ${e}`, severity: Severity.Warning }); + } + } + override dispose(immediate: boolean = false): void { this._isDisposed = true; if (this._process) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts index 32519a134bb..ace4327b848 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts @@ -13,7 +13,7 @@ import { isLinux, isMacintosh } from 'vs/base/common/platform'; import * as dom from 'vs/base/browser/dom'; import { BrowserFeatures } from 'vs/base/browser/canIUse'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { Action, Separator } from 'vs/base/common/actions'; +import { Action, IAction, Separator } from 'vs/base/common/actions'; import { IMenu, IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -426,7 +426,7 @@ export class TerminalTabbedView extends Disposable { })); } - private _getTabActions(): Action[] { + private _getTabActions(): IAction[] { return [ new Separator(), this._configurationService.inspect(TerminalSettingId.TabsLocation).userValue === 'left' ? diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/contextualActionAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/contextualActionAddon.ts new file mode 100644 index 00000000000..f5c38ee5ff1 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/xterm/contextualActionAddon.ts @@ -0,0 +1,167 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { ITerminalCapabilityStore, ITerminalCommand, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; +// Importing types is safe in any layer +// eslint-disable-next-line local/code-import-patterns +import type { ITerminalAddon } from 'xterm-headless'; +import * as dom from 'vs/base/browser/dom'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { ICommandAction, ITerminalContextualActionOptions } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { DecorationSelector, updateLayout } from 'vs/workbench/contrib/terminal/browser/xterm/decorationStyles'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { Terminal } from 'xterm'; +import { IAction } from 'vs/base/common/actions'; + +export interface IContextualAction { + /** + * Shows the quick fix menu + */ + showQuickFixMenu(): void; + + /** + * Registers a listener on onCommandFinished scoped to a particular command or regular + * expression and provides a callback to be executed for commands that match. + */ + registerCommandFinishedListener(options: ITerminalContextualActionOptions): void; +} + +export interface IContextualActionAdddon extends IContextualAction { + onDidRequestRerunCommand: Event<{ command: string; addNewLine?: boolean }>; +} + +export class ContextualActionAddon extends Disposable implements ITerminalAddon, IContextualActionAdddon { + private readonly _onDidRequestRerunCommand = new Emitter<{ command: string; addNewLine?: boolean }>(); + readonly onDidRequestRerunCommand = this._onDidRequestRerunCommand.event; + + private _terminal: Terminal | undefined; + + private _currentQuickFixElement: HTMLElement | undefined; + + private _decorationMarkerIds = new Set(); + + private _commandListeners: Map = new Map(); + + private _matchActions: ICommandAction[] | undefined; + + constructor(private readonly _capabilities: ITerminalCapabilityStore, + @IContextMenuService private readonly _contextMenuService: IContextMenuService, + @IConfigurationService private readonly _configurationService: IConfigurationService) { + super(); + const commandDetectionCapability = this._capabilities.get(TerminalCapability.CommandDetection); + if (commandDetectionCapability) { + this._registerCommandFinishedHandler(); + } else { + this._capabilities.onDidAddCapability(c => { + if (c === TerminalCapability.CommandDetection) { + this._registerCommandFinishedHandler(); + } + }); + } + } + activate(terminal: Terminal): void { + this._terminal = terminal; + } + + showQuickFixMenu(): void { + this._currentQuickFixElement?.click(); + } + + registerCommandFinishedListener(options: ITerminalContextualActionOptions): void { + const matcherKey = options.commandLineMatcher.toString(); + const currentOptions = this._commandListeners.get(matcherKey) || []; + currentOptions.push(options); + this._commandListeners.set(matcherKey, currentOptions); + } + + private _registerCommandFinishedHandler(): void { + const terminal = this._terminal; + const commandDetection = this._capabilities.get(TerminalCapability.CommandDetection); + if (!terminal || !commandDetection) { + return; + } + this._register(commandDetection.onCommandFinished(async command => { + this._matchActions = getMatchActions(command, this._commandListeners, this._onDidRequestRerunCommand); + })); + // The buffer is not ready by the time command finish + // is called. Add the decoration on command start using the actions, if any, + // from the last command + this._register(commandDetection.onCommandStarted(() => { + if (this._matchActions) { + this._registerContextualDecoration(); + this._matchActions = undefined; + } + })); + } + + private _registerContextualDecoration(): void { + if (!this._terminal) { + return; + } + const marker = this._terminal.registerMarker(); + if (!marker) { + return; + } + const actions = this._matchActions; + const decoration = this._terminal.registerDecoration({ marker, layer: 'top' }); + decoration?.onRender((e: HTMLElement) => { + if (!this._decorationMarkerIds.has(decoration.marker.id)) { + this._currentQuickFixElement = e; + e.classList.add(DecorationSelector.QuickFix, DecorationSelector.Codicon, DecorationSelector.CommandDecoration, DecorationSelector.XtermDecoration); + e.style.color = '#ffcc00'; + updateLayout(this._configurationService, e); + if (actions) { + this._decorationMarkerIds.add(decoration.marker.id); + dom.addDisposableListener(e, dom.EventType.CLICK, () => { + this._contextMenuService.showContextMenu({ getAnchor: () => e, getActions: () => actions }); + this._contextMenuService.onDidHideContextMenu(() => decoration.dispose()); + }); + } + } + }); + } +} + +export function getMatchActions(command: ITerminalCommand, actionOptions: Map, onDidRequestRerunCommand?: Emitter<{ command: string; addNewLine?: boolean }>): IAction[] | undefined { + const matchActions: IAction[] = []; + const newCommand = command.command; + for (const options of actionOptions.values()) { + for (const actionOption of options) { + if (actionOption.exitStatus !== undefined && actionOption.exitStatus !== (command.exitCode === 0)) { + continue; + } + const commandLineMatch = newCommand.match(actionOption.commandLineMatcher); + if (!commandLineMatch) { + continue; + } + const outputMatcher = actionOption.outputMatcher; + let outputMatch; + if (outputMatcher) { + outputMatch = command.getOutputMatch(outputMatcher); + } + const actions = actionOption.getActions({ commandLineMatch, outputMatch }, command); + if (!actions) { + return matchActions.length === 0 ? undefined : matchActions; + } + for (const a of actions) { + matchActions.push({ + id: a.id, + label: a.label, + class: a.class, + enabled: a.enabled, + run: async () => { + await a.run(); + if (a.commandToRunInTerminal) { + onDidRequestRerunCommand?.fire({ command: a.commandToRunInTerminal, addNewLine: a.addNewLine }); + } + }, + tooltip: a.tooltip + }); + } + } + } + return matchActions.length === 0 ? undefined : matchActions; +} diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts index c86855c4138..e520738108a 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts @@ -28,22 +28,7 @@ import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/commo import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { terminalDecorationError, terminalDecorationIncomplete, terminalDecorationMark, terminalDecorationSuccess } from 'vs/workbench/contrib/terminal/browser/terminalIcons'; import { TaskSettingId } from 'vs/workbench/contrib/tasks/common/tasks'; - -const enum DecorationSelector { - CommandDecoration = 'terminal-command-decoration', - Hide = 'hide', - ErrorColor = 'error', - DefaultColor = 'default-color', - Default = 'default', - Codicon = 'codicon', - XtermDecoration = 'xterm-decoration', - OverviewRuler = '.xterm-decoration-overview-ruler' -} - -const enum DecorationStyles { - DefaultDimension = 16, - MarginLeft = -17, -} +import { DecorationSelector, updateLayout } from 'vs/workbench/contrib/terminal/browser/xterm/decorationStyles'; interface IDisposableDecoration { decoration: IDecoration; disposables: IDisposable[]; exitCode?: number; markProperties?: IMarkProperties } @@ -170,9 +155,9 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { } public refreshLayouts(): void { - this._updateLayout(this._placeholderDecoration?.element); + updateLayout(this._configurationService, this._placeholderDecoration?.element); for (const decoration of this._decorations) { - this._updateLayout(decoration[1].decoration.element); + updateLayout(this._configurationService, decoration[1].decoration.element); } } @@ -313,7 +298,7 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { } if (!element.classList.contains(DecorationSelector.Codicon) || command?.marker?.line === 0) { // first render or buffer was cleared - this._updateLayout(element); + updateLayout(this._configurationService, element); this._updateClasses(element, command?.exitCode, command?.markProperties || markProperties); } }); @@ -329,23 +314,6 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { return [this._createContextMenu(element, command), ...this._createHover(element, command)]; } - private _updateLayout(element?: HTMLElement): void { - if (!element) { - return; - } - const fontSize = this._configurationService.inspect(TerminalSettingId.FontSize).value; - const defaultFontSize = this._configurationService.inspect(TerminalSettingId.FontSize).defaultValue; - const lineHeight = this._configurationService.inspect(TerminalSettingId.LineHeight).value; - if (typeof fontSize === 'number' && typeof defaultFontSize === 'number' && typeof lineHeight === 'number') { - const scalar = (fontSize / defaultFontSize) <= 1 ? (fontSize / defaultFontSize) : 1; - // must be inlined to override the inlined styles from xterm - element.style.width = `${scalar * DecorationStyles.DefaultDimension}px`; - element.style.height = `${scalar * DecorationStyles.DefaultDimension * lineHeight}px`; - element.style.fontSize = `${scalar * DecorationStyles.DefaultDimension}px`; - element.style.marginLeft = `${scalar * DecorationStyles.MarginLeft}px`; - } - } - private _updateClasses(element?: HTMLElement, exitCode?: number, markProperties?: IMarkProperties): void { if (!element) { return; @@ -444,7 +412,12 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { const labelText = localize("terminal.copyOutput", 'Copy Output'); actions.push({ class: undefined, tooltip: labelText, id: 'terminal.copyOutput', label: labelText, enabled: true, - run: () => this._clipboardService.writeText(command.getOutput()!) + run: () => { + const text = command.getOutput(); + if (typeof text === 'string') { + this._clipboardService.writeText(text); + } + } }); const labelHtml = localize("terminal.copyOutputAsHtml", 'Copy Output as HTML'); actions.push({ diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/decorationStyles.ts b/src/vs/workbench/contrib/terminal/browser/xterm/decorationStyles.ts new file mode 100644 index 00000000000..5f3a59ec230 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/xterm/decorationStyles.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; + +const enum DecorationStyles { + DefaultDimension = 16, + MarginLeft = -17, +} + +export const enum DecorationSelector { + CommandDecoration = 'terminal-command-decoration', + Hide = 'hide', + ErrorColor = 'error', + DefaultColor = 'default-color', + Default = 'default', + Codicon = 'codicon', + XtermDecoration = 'xterm-decoration', + OverviewRuler = '.xterm-decoration-overview-ruler', + QuickFix = 'codicon-light-bulb' +} + +export function updateLayout(configurationService: IConfigurationService, element?: HTMLElement): void { + if (!element) { + return; + } + const fontSize = configurationService.inspect(TerminalSettingId.FontSize).value; + const defaultFontSize = configurationService.inspect(TerminalSettingId.FontSize).defaultValue; + const lineHeight = configurationService.inspect(TerminalSettingId.LineHeight).value; + if (typeof fontSize === 'number' && typeof defaultFontSize === 'number' && typeof lineHeight === 'number') { + const scalar = (fontSize / defaultFontSize) <= 1 ? (fontSize / defaultFontSize) : 1; + // must be inlined to override the inlined styles from xterm + element.style.width = `${scalar * DecorationStyles.DefaultDimension}px`; + element.style.height = `${scalar * DecorationStyles.DefaultDimension * lineHeight}px`; + element.style.fontSize = `${scalar * DecorationStyles.DefaultDimension}px`; + element.style.marginLeft = `${scalar * DecorationStyles.MarginLeft}px`; + } +} + diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index 943d30d0fa0..892e5411f86 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -90,6 +90,7 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II // Always on addons private _markNavigationAddon: MarkNavigationAddon; private _shellIntegrationAddon: ShellIntegrationAddon; + private _decorationAddon: DecorationAddon; // Optional addons @@ -102,8 +103,10 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II private _lastFindResult: { resultIndex: number; resultCount: number } | undefined; get findResult(): { resultIndex: number; resultCount: number } | undefined { return this._lastFindResult; } - private readonly _onDidRequestRunCommand = new Emitter<{ command: ITerminalCommand; copyAsHtml?: boolean }>(); + private readonly _onDidRequestRunCommand = new Emitter<{ command: ITerminalCommand; copyAsHtml?: boolean; noNewLine?: boolean }>(); readonly onDidRequestRunCommand = this._onDidRequestRunCommand.event; + private readonly _onDidRequestFreePort = new Emitter(); + readonly onDidRequestFreePort = this._onDidRequestFreePort.event; private readonly _onDidChangeFindResults = new Emitter<{ resultIndex: number; resultCount: number } | undefined>(); readonly onDidChangeFindResults = this._onDidChangeFindResults.event; private readonly _onDidChangeSelection = new Emitter(); diff --git a/src/vs/workbench/contrib/terminal/common/environmentVariableService.ts b/src/vs/workbench/contrib/terminal/common/environmentVariableService.ts index 834986a1e61..8a7eb1c7c0b 100644 --- a/src/vs/workbench/contrib/terminal/common/environmentVariableService.ts +++ b/src/vs/workbench/contrib/terminal/common/environmentVariableService.ts @@ -102,8 +102,7 @@ export class EnvironmentVariableService implements IEnvironmentVariableService { private async _invalidateExtensionCollections(): Promise { await this._extensionService.whenInstalledExtensionsRegistered(); - - const registeredExtensions = await this._extensionService.getExtensions(); + const registeredExtensions = this._extensionService.extensions; let changes = false; this.collections.forEach((_, extensionIdentifier) => { const isExtensionRegistered = registeredExtensions.some(r => r.identifier.value === extensionIdentifier); diff --git a/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts b/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts index 377973047c3..23d2d4552b1 100644 --- a/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts +++ b/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts @@ -58,7 +58,6 @@ export interface ICreateTerminalProcessResult { } export class RemoteTerminalChannelClient implements IPtyHostController { - get onPtyHostExit(): Event { return this._channel.listen('$onPtyHostExitEvent'); } @@ -238,7 +237,9 @@ export class RemoteTerminalChannelClient implements IPtyHostController { sendCommandResult(reqId: number, isError: boolean, payload: any): Promise { return this._channel.call('$sendCommandResult', [reqId, isError, payload]); } - + freePortKillProcess(id: number, port: string): Promise<{ port: string; processId: string }> { + return this._channel.call('$freePortKillProcess', [id, port]); + } installAutoReply(match: string, reply: string): Promise { return this._channel.call('$installAutoReply', [match, reply]); } diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index a93e196bf1a..e886f17e418 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -96,6 +96,13 @@ export interface IShellLaunchConfigResolveOptions { allowAutomationShell?: boolean; } +export interface ITerminalOutputMatcher { + lineMatcher: string | RegExp; + anchor?: 'top' | 'bottom'; + offset?: number; + length?: number; +} + export interface ITerminalBackend { readonly remoteAuthority: string | undefined; @@ -347,6 +354,7 @@ export interface ITerminalCommand { markProperties?: IMarkProperties; hasOutput(): boolean; getOutput(): string | undefined; + getOutputMatch(outputMatcher: ITerminalOutputMatcher): RegExpMatchArray | undefined; } export interface INavigationMode { @@ -413,6 +421,7 @@ export interface ITerminalProcessManager extends IDisposable { refreshProperty(type: T): Promise; updateProperty(property: T, value: IProcessPropertyMap[T]): void; getBackendOS(): Promise; + freePortKillProcess(port: string): void; } export const enum ProcessState { @@ -510,7 +519,7 @@ export const enum TerminalCommandId { ResizePaneLeft = 'workbench.action.terminal.resizePaneLeft', ResizePaneRight = 'workbench.action.terminal.resizePaneRight', ResizePaneUp = 'workbench.action.terminal.resizePaneUp', - CreateWithProfileButton = 'workbench.action.terminal.createProfileButton', + CreateWithProfileButton = 'workbench.action.terminal.gitCreateProfileButton', SizeToContentWidth = 'workbench.action.terminal.sizeToContentWidth', SizeToContentWidthInstance = 'workbench.action.terminal.sizeToContentWidthInstance', ResizePaneDown = 'workbench.action.terminal.resizePaneDown', @@ -552,6 +561,7 @@ export const enum TerminalCommandId { SelectToNextLine = 'workbench.action.terminal.selectToNextLine', ToggleEscapeSequenceLogging = 'toggleEscapeSequenceLogging', SendSequence = 'workbench.action.terminal.sendSequence', + QuickFix = 'workbench.action.terminal.quickFix', ToggleFindRegex = 'workbench.action.terminal.toggleFindRegex', ToggleFindWholeWord = 'workbench.action.terminal.toggleFindWholeWord', ToggleFindCaseSensitive = 'workbench.action.terminal.toggleFindCaseSensitive', diff --git a/src/vs/workbench/contrib/terminal/electron-sandbox/localPty.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/localPty.ts index 3556ce4b6be..dea8aba884e 100644 --- a/src/vs/workbench/contrib/terminal/electron-sandbox/localPty.ts +++ b/src/vs/workbench/contrib/terminal/electron-sandbox/localPty.ts @@ -76,6 +76,12 @@ export class LocalPty extends Disposable implements ITerminalChildProcess { } this._localPtyService.resize(this.id, cols, rows); } + freePortKillProcess(port: string): Promise<{ port: string; processId: string }> { + if (!this._localPtyService.freePortKillProcess) { + throw new Error('freePortKillProcess does not exist on the local pty service'); + } + return this._localPtyService.freePortKillProcess(this.id, port); + } async getInitialCwd(): Promise { return this._properties.initialCwd; } diff --git a/src/vs/workbench/contrib/terminal/test/browser/capabilities/commandDetectionCapability.test.ts b/src/vs/workbench/contrib/terminal/test/browser/capabilities/commandDetectionCapability.test.ts index 00dfbccf937..1a9e2ab90b2 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/capabilities/commandDetectionCapability.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/capabilities/commandDetectionCapability.test.ts @@ -7,8 +7,11 @@ import { deepStrictEqual, ok } from 'assert'; import { timeout } from 'vs/base/common/async'; import { Terminal } from 'xterm'; import { CommandDetectionCapability } from 'vs/platform/terminal/common/capabilities/commandDetectionCapability'; -import { NullLogService } from 'vs/platform/log/common/log'; +import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { ITerminalCommand } from 'vs/platform/terminal/common/capabilities/capabilities'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { IContextMenuDelegate } from 'vs/base/browser/contextmenu'; async function writeP(terminal: Terminal, data: string): Promise { return new Promise((resolve, reject) => { @@ -64,7 +67,10 @@ suite('CommandDetectionCapability', () => { setup(() => { xterm = new Terminal({ allowProposedApi: true, cols: 80 }); - capability = new TestCommandDetectionCapability(xterm, new NullLogService()); + const instantiationService = new TestInstantiationService(); + instantiationService.stub(IContextMenuService, { showContextMenu(delegate: IContextMenuDelegate): void { } } as Partial); + instantiationService.stub(ILogService, new NullLogService()); + capability = instantiationService.createInstance(TestCommandDetectionCapability, xterm); addEvents = []; capability.onCommandFinished(e => addEvents.push(e)); assertCommands([]); diff --git a/src/vs/workbench/contrib/terminal/test/browser/contextualActionAddon.test.ts b/src/vs/workbench/contrib/terminal/test/browser/contextualActionAddon.test.ts new file mode 100644 index 00000000000..d317f678d19 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/test/browser/contextualActionAddon.test.ts @@ -0,0 +1,237 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { strictEqual } from 'assert'; +import { OpenerService } from 'vs/editor/browser/services/openerService'; +import { ContextMenuService } from 'vs/platform/contextview/browser/contextMenuService'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ILogService, NullLogService } from 'vs/platform/log/common/log'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { ITerminalCommand, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; +import { CommandDetectionCapability } from 'vs/platform/terminal/common/capabilities/commandDetectionCapability'; +import { TerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore'; +import { ICommandAction, ITerminalInstance, ITerminalOutputMatcher } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { freePort, FreePortOutputRegex, gitCreatePr, GitCreatePrOutputRegex, GitPushOutputRegex, gitPushSetUpstream, gitSimilarCommand, GitSimilarOutputRegex } from 'vs/workbench/contrib/terminal/browser/terminalBaseContextualActions'; +import { ContextualActionAddon, getMatchActions } from 'vs/workbench/contrib/terminal/browser/xterm/contextualActionAddon'; +import { Terminal } from 'xterm'; + +suite('ContextualActionAddon', () => { + let contextualActionAddon: ContextualActionAddon; + let terminalInstance: Pick; + let commandDetection: CommandDetectionCapability; + let openerService: OpenerService; + setup(() => { + const instantiationService = new TestInstantiationService(); + const xterm = new Terminal({ + allowProposedApi: true, + cols: 80, + rows: 30 + }); + const capabilities = new TerminalCapabilityStore(); + instantiationService.stub(ILogService, new NullLogService()); + commandDetection = instantiationService.createInstance(CommandDetectionCapability, xterm); + capabilities.add(TerminalCapability.CommandDetection, commandDetection); + instantiationService.stub(IContextMenuService, instantiationService.createInstance(ContextMenuService)); + openerService = instantiationService.createInstance(OpenerService); + instantiationService.stub(IOpenerService, openerService); + terminalInstance = { + async freePortKillProcess(port: string): Promise { } + } as Pick; + contextualActionAddon = instantiationService.createInstance(ContextualActionAddon, capabilities); + xterm.loadAddon(contextualActionAddon); + }); + suite('registerCommandFinishedListener & getMatchActions', () => { + suite('gitSimilarCommand', async () => { + const expectedMap = new Map(); + const command = `git sttatus`; + const output = `git: 'sttatus' is not a git command. See 'git --help'. + + The most similar command is + status`; + const exitCode = 1; + const actions = [ + { + id: 'terminal.gitSimilarCommand', + label: 'Run git status', + run: true, + tooltip: 'Run git status', + enabled: true + } + ]; + setup(() => { + const command = gitSimilarCommand(); + expectedMap.set(command.commandLineMatcher.toString(), [command]); + contextualActionAddon.registerCommandFinishedListener(command); + }); + suite('returns undefined when', () => { + test('output does not match', () => { + strictEqual(getMatchActions(createCommand(command, `invalid output`, GitSimilarOutputRegex, exitCode), expectedMap), undefined); + }); + test('command does not match', () => { + strictEqual(getMatchActions(createCommand(`gt sttatus`, output, GitSimilarOutputRegex, exitCode), expectedMap), undefined); + }); + }); + suite('returns undefined when', () => { + test('expected unix exit code', () => { + assertMatchOptions(getMatchActions(createCommand(command, output, GitSimilarOutputRegex, exitCode), expectedMap), actions); + }); + test('matching exit status', () => { + assertMatchOptions(getMatchActions(createCommand(command, output, GitSimilarOutputRegex, 2), expectedMap), actions); + }); + }); + }); + suite('freePort', () => { + const expected = new Map(); + const portCommand = `yarn start dev`; + const output = `yarn run v1.22.17 + warning ../../package.json: No license field + Error: listen EADDRINUSE: address already in use 0.0.0.0:3000 + at Server.setupListenHandle [as _listen2] (node:net:1315:16) + at listenInCluster (node:net:1363:12) + at doListen (node:net:1501:7) + at processTicksAndRejections (node:internal/process/task_queues:84:21) + Emitted 'error' event on WebSocketServer instance at: + at Server.emit (node:events:394:28) + at emitErrorNT (node:net:1342:8) + at processTicksAndRejections (node:internal/process/task_queues:83:21) { + } + error Command failed with exit code 1. + info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.`; + const actionOptions = [{ + id: 'terminal.freePort', + label: 'Free port 3000', + run: true, + tooltip: 'Free port 3000', + enabled: true + }]; + setup(() => { + const command = freePort(terminalInstance); + expected.set(command.commandLineMatcher.toString(), [command]); + contextualActionAddon.registerCommandFinishedListener(command); + }); + suite('returns undefined when', () => { + test('output does not match', () => { + strictEqual(getMatchActions(createCommand(portCommand, `invalid output`, FreePortOutputRegex), expected), undefined); + }); + }); + test.skip('returns actions', () => { + assertMatchOptions(getMatchActions(createCommand(portCommand, output, FreePortOutputRegex), expected), actionOptions); + }); + }); + suite('gitPushSetUpstream', () => { + const expectedMap = new Map(); + const command = `git push`; + const output = `fatal: The current branch test22 has no upstream branch. + To push the current branch and set the remote as upstream, use + + git push --set-upstream origin test22 `; + const exitCode = 128; + const actions = [ + { + id: 'terminal.gitPush', + label: 'Git push test22', + run: true, + tooltip: 'Git push test22', + enabled: true + } + ]; + setup(() => { + const command = gitPushSetUpstream(); + expectedMap.set(command.commandLineMatcher.toString(), [command]); + contextualActionAddon.registerCommandFinishedListener(command); + }); + suite('returns undefined when', () => { + test('output does not match', () => { + strictEqual(getMatchActions(createCommand(command, `invalid output`, GitPushOutputRegex, exitCode), expectedMap), undefined); + }); + test('command does not match', () => { + strictEqual(getMatchActions(createCommand(`git status`, output, GitPushOutputRegex, exitCode), expectedMap), undefined); + }); + }); + suite('returns undefined when', () => { + test('expected unix exit code', () => { + assertMatchOptions(getMatchActions(createCommand(command, output, GitPushOutputRegex, exitCode), expectedMap), actions); + }); + test('matching exit status', () => { + assertMatchOptions(getMatchActions(createCommand(command, output, GitPushOutputRegex, 2), expectedMap), actions); + }); + }); + }); + suite('gitCreatePr', () => { + const expectedMap = new Map(); + const command = `git push`; + const output = `Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 + remote: + remote: Create a pull request for 'test22' on GitHub by visiting: + remote: https://github.com/meganrogge/xterm.js/pull/new/test22 + remote: + To https://github.com/meganrogge/xterm.js + * [new branch] test22 -> test22 + Branch 'test22' set up to track remote branch 'test22' from 'origin'. `; + const exitCode = 0; + const actions = [ + { + id: 'terminal.gitCreatePr', + label: 'Create PR', + run: true, + tooltip: 'Create PR', + enabled: true + } + ]; + setup(() => { + const command = gitCreatePr(openerService); + expectedMap.set(command.commandLineMatcher.toString(), [command]); + contextualActionAddon.registerCommandFinishedListener(command); + }); + suite('returns undefined when', () => { + test('output does not match', () => { + strictEqual(getMatchActions(createCommand(command, `invalid output`, GitCreatePrOutputRegex, exitCode), expectedMap), undefined); + }); + test('command does not match', () => { + strictEqual(getMatchActions(createCommand(`git status`, output, GitCreatePrOutputRegex, exitCode), expectedMap), undefined); + }); + test('failure exit status', () => { + strictEqual(getMatchActions(createCommand(command, output, GitCreatePrOutputRegex, 2), expectedMap), undefined); + }); + }); + suite('returns actions when', () => { + test('expected unix exit code', () => { + assertMatchOptions(getMatchActions(createCommand(command, output, GitCreatePrOutputRegex, exitCode), expectedMap), actions); + }); + }); + }); + }); +}); + +function createCommand(command: string, output: string, outputMatcher?: RegExp | string, exitCode?: number): ITerminalCommand { + return { + command, + exitCode, + getOutput: () => { return output; }, + getOutputMatch: (matcher: ITerminalOutputMatcher) => { + if (outputMatcher) { + return output.match(outputMatcher) ?? undefined; + } + return undefined; + }, + timestamp: Date.now(), + hasOutput: () => !!output + }; +} + +function assertMatchOptions(actual: ICommandAction[] | undefined, expected: { id: string; label: string; run: boolean; tooltip: string; enabled: boolean }[]): void { + strictEqual(actual?.length, expected.length); + let index = 0; + for (const i of actual) { + const j = expected[index]; + strictEqual(i.id, j.id, `ID`); + strictEqual(i.enabled, j.enabled, `enabled`); + strictEqual(i.label, j.label, `label`); + strictEqual(!!i.run, j.run, `run`); + strictEqual(i.tooltip, j.tooltip, `tooltip`); + index++; + } +} diff --git a/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkOpeners.test.ts b/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkOpeners.test.ts index 54d0d6d4c74..4aaa1f4fedf 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkOpeners.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkOpeners.test.ts @@ -25,6 +25,7 @@ import { TestContextService } from 'vs/workbench/test/common/workbenchTestServic import { Terminal } from 'xterm'; import { IFileQuery, ISearchComplete, ISearchService } from 'vs/workbench/services/search/common/search'; import { SearchService } from 'vs/workbench/services/search/common/searchService'; +import { ITerminalOutputMatcher } from 'vs/workbench/contrib/terminal/common/terminal'; export interface ITerminalLinkActivationResult { source: 'editor' | 'search'; @@ -131,6 +132,7 @@ suite('Workbench - TerminalLinkOpeners', () => { cwd: '/initial/cwd', timestamp: 0, getOutput() { return undefined; }, + getOutputMatch(outputMatcher: ITerminalOutputMatcher) { return undefined; }, marker: { line: 0 } as Partial as any, @@ -267,6 +269,7 @@ suite('Workbench - TerminalLinkOpeners', () => { cwd, timestamp: 0, getOutput() { return undefined; }, + getOutputMatch(outputMatcher: ITerminalOutputMatcher) { return undefined; }, marker: { line: 0 } as Partial as any, @@ -351,6 +354,7 @@ suite('Workbench - TerminalLinkOpeners', () => { cwd, timestamp: 0, getOutput() { return undefined; }, + getOutputMatch(outputMatcher: ITerminalOutputMatcher) { return undefined; }, marker: { line: 0 } as Partial as any, diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts index 9d89281d10d..1169424ac82 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts @@ -21,7 +21,7 @@ import { TerminalCapabilityStore } from 'vs/platform/terminal/common/capabilitie import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; import { Schemas } from 'vs/base/common/network'; -function createInstance(partial?: Partial): Pick { +export function createInstance(partial?: Partial): Pick { const capabilities = new TerminalCapabilityStore(); if (!isWindows) { capabilities.add(TerminalCapability.NaiveCwdDetection, null!); diff --git a/src/vs/workbench/contrib/terminal/test/browser/xterm/decorationAddon.test.ts b/src/vs/workbench/contrib/terminal/test/browser/xterm/decorationAddon.test.ts index cf3ddc47e58..335b810d5b1 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/xterm/decorationAddon.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/xterm/decorationAddon.test.ts @@ -58,7 +58,7 @@ suite('DecorationAddon', () => { instantiationService.stub(IConfigurationService, configurationService); instantiationService.stub(IContextMenuService, instantiationService.createInstance(ContextMenuService)); const capabilities = new TerminalCapabilityStore(); - capabilities.add(TerminalCapability.CommandDetection, new CommandDetectionCapability(xterm, new NullLogService())); + capabilities.add(TerminalCapability.CommandDetection, instantiationService.createInstance(CommandDetectionCapability, xterm)); instantiationService.stub(ILifecycleService, new TestLifecycleService()); decorationAddon = instantiationService.createInstance(DecorationAddon, capabilities); xterm.loadAddon(decorationAddon); diff --git a/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.test.ts b/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.test.ts index 5e3442d462c..f30df87be77 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.test.ts @@ -45,7 +45,7 @@ suite('ShellIntegrationAddon', () => { xterm = new Terminal({ allowProposedApi: true, cols: 80, rows: 30 }); const instantiationService = new TestInstantiationService(); instantiationService.stub(ILogService, NullLogService); - shellIntegrationAddon = instantiationService.createInstance(TestShellIntegrationAddon, undefined, undefined); + shellIntegrationAddon = instantiationService.createInstance(TestShellIntegrationAddon); xterm.loadAddon(shellIntegrationAddon); capabilities = shellIntegrationAddon.capabilities; }); diff --git a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts index 5c50f064381..7287bcccafa 100644 --- a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts @@ -115,14 +115,13 @@ export class TestingDecorationService extends Disposable implements ITestingDeco // is up to date. This prevents issues, as in #138632, #138835, #138922. this._register(this.testService.onWillProcessDiff(diff => { for (const entry of diff) { - if (entry.op !== TestDiffOpType.Update || entry.item.docv === undefined || entry.item.item?.range === undefined) { + if (entry.op !== TestDiffOpType.DocumentSynced) { continue; } - const uri = this.testService.collection.getNodeById(entry.item.extId)?.item.uri; - const rec = uri && this.decorationCache.get(uri); + const rec = this.decorationCache.get(entry.uri); if (rec) { - rec.rangeUpdateVersionId = entry.item.docv; + rec.rangeUpdateVersionId = entry.docv; } } @@ -193,7 +192,7 @@ export class TestingDecorationService extends Disposable implements ITestingDeco const uriStr = model.uri.toString(); const cached = this.decorationCache.get(model.uri); const testRangesUpdated = cached?.rangeUpdateVersionId === model.getVersionId(); - const lastDecorations = cached?.value ?? new TestDecorations(); + const lastDecorations = cached?.value ?? new TestDecorations(); const newDecorations = new TestDecorations(); model.changeDecorations(accessor => { @@ -210,7 +209,7 @@ export class TestingDecorationService extends Disposable implements ITestingDeco for (const [line, tests] of runDecorations.lines()) { const multi = tests.length > 1; - let existing = lastDecorations.findOnLine(line, d => multi ? d instanceof MultiRunTestDecoration : d instanceof RunSingleTestDecoration) as RunTestDecoration | undefined; + let existing = lastDecorations.value.find(d => d instanceof RunTestDecoration && d.exactlyContainsTests(tests)) as RunTestDecoration | undefined; // see comment in the constructor for what's going on here if (existing && testRangesUpdated && model.getDecorationRange(existing.id)?.startLineNumber !== line) { @@ -652,6 +651,27 @@ abstract class RunTestDecoration { return true; } + public exactlyContainsTests(tests: readonly { test: IncrementalTestCollectionItem }[]): boolean { + if (tests.length !== this.tests.length) { + return false; + } + if (tests.length === 1) { + return tests[0].test.item.extId === this.tests[0].test.item.extId; + } + + const ownTests = new Set(); + for (const t of this.tests) { + ownTests.add(t.test.item.extId); + } + for (const t of tests) { + if (!ownTests.delete(t.test.item.extId)) { + return false; + } + } + + return true; + } + /** * Updates the decoration to match the new set of tests. * @returns true if options were changed, false otherwise diff --git a/src/vs/workbench/contrib/testing/common/testItemCollection.ts b/src/vs/workbench/contrib/testing/common/testItemCollection.ts index 4d55e567807..34ef310d046 100644 --- a/src/vs/workbench/contrib/testing/common/testItemCollection.ts +++ b/src/vs/workbench/contrib/testing/common/testItemCollection.ts @@ -33,6 +33,7 @@ export const enum TestItemEventOp { RemoveChild, SetProp, Bulk, + DocumentSynced, } export interface ITestItemUpsertChild { @@ -65,13 +66,18 @@ export interface ITestItemBulkReplace { ops: (ITestItemUpsertChild | ITestItemRemoveChild)[]; } +export interface ITestItemDocumentSynced { + op: TestItemEventOp.DocumentSynced; +} + export type ExtHostTestItemEvent = | ITestItemSetTags | ITestItemUpsertChild | ITestItemRemoveChild | ITestItemUpdateCanResolveChildren | ITestItemSetProp - | ITestItemBulkReplace; + | ITestItemBulkReplace + | ITestItemDocumentSynced; export interface ITestItemApi { controllerId: string; @@ -201,17 +207,32 @@ export class TestItemCollection extends Disposable { * Pushes a new diff entry onto the collected diff list. */ public pushDiff(diff: TestsDiffOp) { - // Try to merge updates, since they're invoked per-property - const last = this.diff[this.diff.length - 1]; - if (last && diff.op === TestDiffOpType.Update) { - if (last.op === TestDiffOpType.Update && last.item.extId === diff.item.extId) { - applyTestItemUpdate(last.item, diff.item); - return; - } + switch (diff.op) { + case TestDiffOpType.DocumentSynced: { + for (const existing of this.diff) { + if (existing.op === TestDiffOpType.DocumentSynced && existing.uri === diff.uri) { + existing.docv = diff.docv; + return; + } + } - if (last.op === TestDiffOpType.Add && last.item.item.extId === diff.item.extId) { - applyTestItemUpdate(last.item, diff.item); - return; + break; + } + case TestDiffOpType.Update: { + // Try to merge updates, since they're invoked per-property + const last = this.diff[this.diff.length - 1]; + if (last) { + if (last.op === TestDiffOpType.Update && last.item.extId === diff.item.extId) { + applyTestItemUpdate(last.item, diff.item); + return; + } + + if (last.op === TestDiffOpType.Add && last.item.item.extId === diff.item.extId) { + applyTestItemUpdate(last.item, diff.item); + return; + } + } + break; } } @@ -291,15 +312,29 @@ export class TestItemCollection extends Disposable { item: { extId: internal.fullId.toString(), item: evt.update, - docv: this.options.getDocumentVersion(internal.actual.uri), } }); break; + + case TestItemEventOp.DocumentSynced: + this.documentSynced(internal.actual.uri); + break; + default: assertNever(evt); } } + private documentSynced(uri: URI | undefined) { + if (uri) { + this.pushDiff({ + op: TestDiffOpType.DocumentSynced, + uri, + docv: this.options.getDocumentVersion(uri) + }); + } + } + private upsertItem(actual: T, parent: CollectionItem | undefined) { const fullId = TestId.fromExtHostTestItem(actual, this.root.id, parent?.actual); @@ -370,6 +405,9 @@ export class TestItemCollection extends Disposable { this.removeItem(TestId.joinToString(fullId, child.id)); } } + + // Mark ranges in the document as synced (#161320) + this.documentSynced(internal.actual.uri); } private diffTagRefs(newTags: readonly ITestTag[], oldTags: readonly ITestTag[], extId: string) { diff --git a/src/vs/workbench/contrib/testing/common/testTypes.ts b/src/vs/workbench/contrib/testing/common/testTypes.ts index 7edb36fb6e3..bf2ac6d4ba6 100644 --- a/src/vs/workbench/contrib/testing/common/testTypes.ts +++ b/src/vs/workbench/contrib/testing/common/testTypes.ts @@ -369,12 +369,6 @@ export interface ITestItemUpdate { extId: string; expand?: TestItemExpandState; item?: Partial; - - /** - * The document version at the time the operation was made, if the test has - * a URI and the document was open in the extension host. - */ - docv?: number; } export namespace ITestItemUpdate { @@ -382,7 +376,6 @@ export namespace ITestItemUpdate { extId: string; expand?: TestItemExpandState; item?: Partial; - docv?: number; } export const serialize = (u: ITestItemUpdate): Serialized => { @@ -399,7 +392,7 @@ export namespace ITestItemUpdate { if (u.item.sortText !== undefined) { item.sortText = u.item.sortText; } } - return { extId: u.extId, expand: u.expand, item, docv: u.docv }; + return { extId: u.extId, expand: u.expand, item }; }; export const deserialize = (u: Serialized): ITestItemUpdate => { @@ -415,7 +408,7 @@ export namespace ITestItemUpdate { if (u.item.sortText !== undefined) { item.sortText = u.item.sortText; } } - return { extId: u.extId, expand: u.expand, item, docv: u.docv }; + return { extId: u.extId, expand: u.expand, item }; }; } @@ -533,6 +526,8 @@ export const enum TestDiffOpType { Add, /** Shallow-updates an existing test */ Update, + /** Ranges of some tests in a document were synced, so it should be considered up-to-date */ + DocumentSynced, /** Removes a test (and all its children) */ Remove, /** Changes the number of controllers who are yet to publish their collection roots. */ @@ -552,7 +547,8 @@ export type TestsDiffOp = | { op: TestDiffOpType.Retire; itemId: string } | { op: TestDiffOpType.IncrementPendingExtHosts; amount: number } | { op: TestDiffOpType.AddTag; tag: ITestTagDisplayInfo } - | { op: TestDiffOpType.RemoveTag; id: string }; + | { op: TestDiffOpType.RemoveTag; id: string } + | { op: TestDiffOpType.DocumentSynced; uri: URI; docv?: number }; export namespace TestsDiffOp { export type Serialized = @@ -562,13 +558,16 @@ export namespace TestsDiffOp { | { op: TestDiffOpType.Retire; itemId: string } | { op: TestDiffOpType.IncrementPendingExtHosts; amount: number } | { op: TestDiffOpType.AddTag; tag: ITestTagDisplayInfo } - | { op: TestDiffOpType.RemoveTag; id: string }; + | { op: TestDiffOpType.RemoveTag; id: string } + | { op: TestDiffOpType.DocumentSynced; uri: UriComponents; docv?: number }; export const deserialize = (u: Serialized): TestsDiffOp => { if (u.op === TestDiffOpType.Add) { return { op: u.op, item: InternalTestItem.deserialize(u.item) }; } else if (u.op === TestDiffOpType.Update) { return { op: u.op, item: ITestItemUpdate.deserialize(u.item) }; + } else if (u.op === TestDiffOpType.DocumentSynced) { + return { op: u.op, uri: URI.revive(u.uri), docv: u.docv }; } else { return u; } diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts index c40a9cb7a63..74775e7c433 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts @@ -3,42 +3,38 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Codicon } from 'vs/base/common/codicons'; -import { Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { isWeb } from 'vs/base/common/platform'; +import { Event } from 'vs/base/common/event'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { localize } from 'vs/nls'; import { Action2, ISubmenuItem, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IProductService } from 'vs/platform/product/common/productService'; import { Registry } from 'vs/platform/registry/common/platform'; -import { registerColor } from 'vs/platform/theme/common/colorRegistry'; -import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; -import { themeColorFromId } from 'vs/platform/theme/common/themeService'; import { IUserDataProfile, IUserDataProfilesService, PROFILES_ENABLEMENT_CONFIG } from 'vs/platform/userDataProfile/common/userDataProfile'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { MangeSettingsProfileAction } from 'vs/workbench/contrib/userDataProfile/browser/userDataProfileActions'; +import { RenameProfileAction } from 'vs/workbench/contrib/userDataProfile/browser/userDataProfileActions'; import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar'; -import { CURRENT_PROFILE_CONTEXT, HAS_PROFILES_CONTEXT, IUserDataProfileManagementService, IUserDataProfileService, ManageProfilesSubMenu, PROFILES_CATEGORY, PROFILES_ENABLEMENT_CONTEXT, PROFILES_TTILE } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; - -export const userDataProfilesIcon = registerIcon('settingsProfiles-icon', Codicon.settings, localize('settingsProfilesIcon', 'Icon for Settings Profiles.')); +import { CURRENT_PROFILE_CONTEXT, HAS_PROFILES_CONTEXT, IS_CURRENT_PROFILE_TRANSIENT_CONTEXT, IUserDataProfileManagementService, IUserDataProfileService, ManageProfilesSubMenu, PROFILES_ENABLEMENT_CONTEXT, PROFILES_TTILE } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { charCount } from 'vs/base/common/strings'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; export class UserDataProfilesWorkbenchContribution extends Disposable implements IWorkbenchContribution { private readonly currentProfileContext: IContextKey; + private readonly isCurrentProfileTransientContext: IContextKey; private readonly hasProfilesContext: IContextKey; constructor( @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @IUserDataProfileManagementService private readonly userDataProfileManagementService: IUserDataProfileManagementService, - @IStatusbarService private readonly statusBarService: IStatusbarService, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IProductService private readonly productService: IProductService, @IContextKeyService contextKeyService: IContextKeyService, @ILifecycleService lifecycleService: ILifecycleService, @@ -48,16 +44,19 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements this.registerConfiguration(); this.currentProfileContext = CURRENT_PROFILE_CONTEXT.bindTo(contextKeyService); + this.isCurrentProfileTransientContext = IS_CURRENT_PROFILE_TRANSIENT_CONTEXT.bindTo(contextKeyService); + this.currentProfileContext.set(this.userDataProfileService.currentProfile.id); - this._register(this.userDataProfileService.onDidChangeCurrentProfile(e => this.currentProfileContext.set(this.userDataProfileService.currentProfile.id))); + this.isCurrentProfileTransientContext.set(!!this.userDataProfileService.currentProfile.isTransient); + this._register(this.userDataProfileService.onDidChangeCurrentProfile(e => { + this.currentProfileContext.set(this.userDataProfileService.currentProfile.id); + this.isCurrentProfileTransientContext.set(!!this.userDataProfileService.currentProfile.isTransient); + })); this.hasProfilesContext = HAS_PROFILES_CONTEXT.bindTo(contextKeyService); this.hasProfilesContext.set(this.userDataProfilesService.profiles.length > 1); this._register(this.userDataProfilesService.onDidChangeProfiles(e => this.hasProfilesContext.set(this.userDataProfilesService.profiles.length > 1))); - this.updateStatus(); - this._register(Event.any(this.workspaceContextService.onDidChangeWorkbenchState, this.userDataProfileService.onDidChangeCurrentProfile, this.userDataProfileService.onDidUpdateCurrentProfile, this.userDataProfilesService.onDidChangeProfiles)(() => this.updateStatus())); - this.registerActions(); if (isWeb) { @@ -87,6 +86,9 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements this.registerProfilesActions(); this._register(this.userDataProfilesService.onDidChangeProfiles(() => this.registerProfilesActions())); + + this.registerCurrentProfilesActions(); + this._register(Event.any(this.userDataProfileService.onDidChangeCurrentProfile, this.userDataProfileService.onDidUpdateCurrentProfile)(() => this.registerCurrentProfilesActions())); } private registerManageProfilesSubMenu(): void { @@ -94,21 +96,21 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { get title() { return localize('manageProfiles', "{0} ({1})", PROFILES_TTILE.value, that.userDataProfileService.currentProfile.name); }, submenu: ManageProfilesSubMenu, - group: '5_profiles', + group: '5_settings', when: PROFILES_ENABLEMENT_CONTEXT, - order: 3 + order: 1 }); MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { title: PROFILES_TTILE, submenu: ManageProfilesSubMenu, - group: '5_profiles', + group: '5_settings', when: PROFILES_ENABLEMENT_CONTEXT, - order: 3 + order: 1 }); MenuRegistry.appendMenuItem(MenuId.AccountsContext, { get title() { return localize('manageProfiles', "{0} ({1})", PROFILES_TTILE.value, that.userDataProfileService.currentProfile.name); }, submenu: ManageProfilesSubMenu, - group: '1_profiles', + group: '1_settings', when: PROFILES_ENABLEMENT_CONTEXT, }); } @@ -146,42 +148,83 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements }); } - private profileStatusAccessor: IStatusbarEntryAccessor | undefined; - private updateStatus(): void { - if (this.userDataProfilesService.profiles.length > 1) { - const statusBarEntry: IStatusbarEntry = { - name: PROFILES_CATEGORY, - command: MangeSettingsProfileAction.ID, - ariaLabel: localize('currentProfile', "Current Settings Profile is {0}", this.userDataProfileService.currentProfile.name), - text: `$(${userDataProfilesIcon.id}) ${this.userDataProfileService.currentProfile.name!}`, - tooltip: localize('profileTooltip', "{0}: {1}", PROFILES_CATEGORY, this.userDataProfileService.currentProfile.name), - color: themeColorFromId(STATUS_BAR_SETTINGS_PROFILE_FOREGROUND), - backgroundColor: themeColorFromId(STATUS_BAR_SETTINGS_PROFILE_BACKGROUND) - }; - if (this.profileStatusAccessor) { - this.profileStatusAccessor.update(statusBarEntry); - } else { - this.profileStatusAccessor = this.statusBarService.addEntry(statusBarEntry, 'status.userDataProfile', StatusbarAlignment.LEFT, Number.MAX_VALUE - 1); - } - } else { - if (this.profileStatusAccessor) { - this.profileStatusAccessor.dispose(); - this.profileStatusAccessor = undefined; - } - } + private readonly currentprofileActionsDisposable = this._register(new MutableDisposable()); + private registerCurrentProfilesActions(): void { + this.currentprofileActionsDisposable.value = new DisposableStore(); + this.currentprofileActionsDisposable.value.add(this.registerUpdateCurrentProfileShortNameAction()); + this.currentprofileActionsDisposable.value.add(this.registerRenameCurrentProfileAction()); } + + private registerUpdateCurrentProfileShortNameAction(): IDisposable { + const that = this; + return registerAction2(class UpdateCurrentProfileShortName extends Action2 { + constructor() { + super({ + id: `workbench.profiles.actions.updateCurrentProfileShortName`, + title: { + value: localize('change short name profile', "Change Short Name ({0})...", that.userDataProfileService.currentProfile.shortName), + original: `Change Short Name (${that.userDataProfileService.currentProfile.shortName})...` + }, + menu: [ + { + id: ManageProfilesSubMenu, + group: '2_manage_current', + when: ContextKeyExpr.and(ContextKeyExpr.notEquals(CURRENT_PROFILE_CONTEXT.key, that.userDataProfilesService.defaultProfile.id), IS_CURRENT_PROFILE_TRANSIENT_CONTEXT.toNegated()), + order: 1 + } + ] + }); + } + async run(accessor: ServicesAccessor) { + const quickInputService = accessor.get(IQuickInputService); + const notificationService = accessor.get(INotificationService); + + const profile = that.userDataProfileService.currentProfile; + const shortName = await quickInputService.input({ + value: profile.shortName, + title: localize('change short name', "Change Short Name..."), + validateInput: async (value: string) => { + if (profile.shortName !== value && !ThemeIcon.fromString(value) && charCount(value) > 2) { + return localize('invalid short name', "Short name should be at most 2 characters long."); + } + return undefined; + } + }); + if (shortName && shortName !== profile.shortName) { + try { + await that.userDataProfileManagementService.updateProfile(profile, { shortName }); + } catch (error) { + notificationService.error(error); + } + } + } + }); + } + + private registerRenameCurrentProfileAction(): IDisposable { + const that = this; + return registerAction2(class RenameCurrentProfileAction extends Action2 { + constructor() { + super({ + id: `workbench.profiles.actions.renameCurrentProfile`, + title: { + value: localize('rename profile', "Rename ({0})...", that.userDataProfileService.currentProfile.name), + original: `Rename (${that.userDataProfileService.currentProfile.name})...` + }, + menu: [ + { + id: ManageProfilesSubMenu, + group: '2_manage_current', + when: ContextKeyExpr.and(ContextKeyExpr.notEquals(CURRENT_PROFILE_CONTEXT.key, that.userDataProfilesService.defaultProfile.id), IS_CURRENT_PROFILE_TRANSIENT_CONTEXT.toNegated()), + order: 2 + } + ] + }); + } + async run(accessor: ServicesAccessor) { + accessor.get(ICommandService).executeCommand(RenameProfileAction.ID, that.userDataProfileService.currentProfile); + } + }); + } + } - -const STATUS_BAR_SETTINGS_PROFILE_FOREGROUND = registerColor('statusBarItem.settingsProfilesForeground', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('statusBarItemSettingsProfileForeground', "Foreground color for the settings profile entry on the status bar.")); - -const STATUS_BAR_SETTINGS_PROFILE_BACKGROUND = registerColor('statusBarItem.settingsProfilesBackground', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, localize('statusBarItemSettingsProfileBackground', "Background color for the settings profile entry on the status bar.")); diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfileActions.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfileActions.ts index 52c7323173f..dbdb4fea927 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfileActions.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfileActions.ts @@ -14,7 +14,7 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { INotificationService } from 'vs/platform/notification/common/notification'; import { QuickPickItem, IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { asJson, asText, IRequestService } from 'vs/platform/request/common/request'; -import { IUserDataProfileTemplate, isUserDataProfileTemplate, IUserDataProfileManagementService, IUserDataProfileImportExportService, PROFILES_CATEGORY, PROFILE_EXTENSION, PROFILE_FILTER, ManageProfilesSubMenu, IUserDataProfileService, PROFILES_ENABLEMENT_CONTEXT, HAS_PROFILES_CONTEXT } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { IUserDataProfileTemplate, isUserDataProfileTemplate, IUserDataProfileManagementService, IUserDataProfileImportExportService, PROFILES_CATEGORY, PROFILE_EXTENSION, PROFILE_FILTER, ManageProfilesSubMenu, IUserDataProfileService, PROFILES_ENABLEMENT_CONTEXT, HAS_PROFILES_CONTEXT, MANAGE_PROFILES_ACTION_ID } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { CATEGORIES } from 'vs/workbench/common/actions'; @@ -123,7 +123,7 @@ registerAction2(class CreateProfileAction extends Action2 { menu: [ { id: ManageProfilesSubMenu, - group: '2_manage_profiles', + group: '3_manage_profiles', when: PROFILES_ENABLEMENT_CONTEXT, order: 1 } @@ -167,10 +167,11 @@ registerAction2(class CreateTransientProfileAction extends Action2 { } }); -registerAction2(class RenameProfileAction extends Action2 { +export class RenameProfileAction extends Action2 { + static readonly ID = 'workbench.profiles.actions.renameProfile'; constructor() { super({ - id: 'workbench.profiles.actions.renameProfile', + id: RenameProfileAction.ID, title: { value: localize('rename profile', "Rename..."), original: 'Rename...' @@ -181,7 +182,7 @@ registerAction2(class RenameProfileAction extends Action2 { menu: [ { id: ManageProfilesSubMenu, - group: '2_manage_profiles', + group: '3_manage_profiles', when: PROFILES_ENABLEMENT_CONTEXT, order: 1 } @@ -189,46 +190,59 @@ registerAction2(class RenameProfileAction extends Action2 { }); } - async run(accessor: ServicesAccessor) { + async run(accessor: ServicesAccessor, profile?: IUserDataProfile) { const quickInputService = accessor.get(IQuickInputService); const userDataProfileService = accessor.get(IUserDataProfileService); const userDataProfilesService = accessor.get(IUserDataProfilesService); const userDataProfileManagementService = accessor.get(IUserDataProfileManagementService); const notificationService = accessor.get(INotificationService); - const profiles = userDataProfilesService.profiles.filter(p => !p.isDefault); - if (profiles.length) { - const pick = await quickInputService.pick( - profiles.map(profile => ({ - label: profile.name, - description: profile.id === userDataProfileService.currentProfile.id ? localize('current', "Current") : undefined, - profile - })), - { - placeHolder: localize('pick profile to rename', "Select Settings Profile to Rename"), - }); - if (pick) { - const name = await quickInputService.input({ - value: pick.profile.name, - title: localize('edit settings profile', "Rename Settings Profile..."), - validateInput: async (value: string) => { - if (pick.profile.name !== value && profiles.some(p => p.name === value)) { - return localize('profileExists', "Settings Profile with name {0} already exists.", value); - } - return undefined; - } - }); - if (name && name !== pick.profile.name) { - try { - await userDataProfileManagementService.renameProfile(pick.profile, name); - } catch (error) { - notificationService.error(error); - } + if (!profile) { + profile = await this.pickProfile(quickInputService, userDataProfileService, userDataProfilesService); + } + + if (!profile || profile.isDefault) { + return; + } + + const name = await quickInputService.input({ + value: profile.name, + title: localize('edit settings profile', "Rename Settings Profile..."), + validateInput: async (value: string) => { + if (profile!.name !== value && userDataProfilesService.profiles.some(p => p.name === value)) { + return localize('profileExists', "Settings Profile with name {0} already exists.", value); } + return undefined; + } + }); + if (name && name !== profile.name) { + try { + await userDataProfileManagementService.updateProfile(profile, { name }); + } catch (error) { + notificationService.error(error); } } } -}); + + private async pickProfile(quickInputService: IQuickInputService, userDataProfileService: IUserDataProfileService, userDataProfilesService: IUserDataProfilesService): Promise { + const profiles = userDataProfilesService.profiles.filter(p => !p.isDefault && !p.isTransient); + if (!profiles.length) { + return undefined; + } + const pick = await quickInputService.pick( + profiles.map(profile => ({ + label: profile.name, + description: profile.id === userDataProfileService.currentProfile.id ? localize('current', "Current") : undefined, + profile + })), + { + placeHolder: localize('pick profile to rename', "Select Settings Profile to Rename"), + }); + return pick?.profile; + } +} + +registerAction2(RenameProfileAction); registerAction2(class DeleteProfileAction extends Action2 { constructor() { @@ -244,7 +258,7 @@ registerAction2(class DeleteProfileAction extends Action2 { menu: [ { id: ManageProfilesSubMenu, - group: '2_manage_profiles', + group: '3_manage_profiles', when: PROFILES_ENABLEMENT_CONTEXT, order: 2 } @@ -259,7 +273,7 @@ registerAction2(class DeleteProfileAction extends Action2 { const userDataProfileManagementService = accessor.get(IUserDataProfileManagementService); const notificationService = accessor.get(INotificationService); - const profiles = userDataProfilesService.profiles.filter(p => !p.isDefault); + const profiles = userDataProfilesService.profiles.filter(p => !p.isDefault && !p.isTransient); if (profiles.length) { const picks = await quickInputService.pick( profiles.map(profile => ({ @@ -282,11 +296,10 @@ registerAction2(class DeleteProfileAction extends Action2 { } }); -export class MangeSettingsProfileAction extends Action2 { - static readonly ID = 'workbench.profiles.actions.manage'; +registerAction2(class ManageSettingsProfileAction extends Action2 { constructor() { super({ - id: MangeSettingsProfileAction.ID, + id: MANAGE_PROFILES_ACTION_ID, title: { value: localize('mange', "Manage..."), original: 'Manage...' @@ -317,14 +330,13 @@ export class MangeSettingsProfileAction extends Action2 { label: `${action.label}${action.checked ? ` $(${Codicon.check.id})` : ''}`, }; }); - const pick = await quickInputService.pick(picks, { canPickMany: false }); + const pick = await quickInputService.pick(picks, { canPickMany: false, title: PROFILES_CATEGORY }); if (pick?.id) { await commandService.executeCommand(pick.id); } } } -} -registerAction2(MangeSettingsProfileAction); +}); registerAction2(class SwitchProfileAction extends Action2 { constructor() { @@ -372,7 +384,7 @@ registerAction2(class ExportProfileAction extends Action2 { menu: [ { id: ManageProfilesSubMenu, - group: '3_import_export_profiles', + group: '4_import_export_profiles', when: PROFILES_ENABLEMENT_CONTEXT, order: 1 }, { @@ -417,7 +429,7 @@ registerAction2(class ImportProfileAction extends Action2 { menu: [ { id: ManageProfilesSubMenu, - group: '3_import_export_profiles', + group: '4_import_export_profiles', when: PROFILES_ENABLEMENT_CONTEXT, order: 2 }, { diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 985972ee280..023bf17fe4c 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -56,6 +56,7 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { ctxIsMergeResultEditor, ctxMergeBaseUri } from 'vs/workbench/contrib/mergeEditor/common/mergeEditor'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; type ConfigureSyncQuickPickItem = { id: SyncResource; label: string; description?: string }; @@ -121,6 +122,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo @IAuthenticationService private readonly authenticationService: IAuthenticationService, @IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, @IConfigurationService private readonly configurationService: IConfigurationService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, @IUserDataInitializationService private readonly userDataInitializationService: IUserDataInitializationService, @IHostService private readonly hostService: IHostService ) { @@ -587,7 +589,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } private getConfigureSyncQuickPickItems(): ConfigureSyncQuickPickItem[] { - return [{ + const result = [{ id: SyncResource.Settings, label: getSyncAreaLabel(SyncResource.Settings) }, { @@ -607,6 +609,13 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo id: SyncResource.GlobalState, label: getSyncAreaLabel(SyncResource.GlobalState), }]; + if (!this.environmentService.isBuilt || (this.productService.enableSyncingProfiles && this.configurationService.getValue('settingsSync.enableSyncingProfiles'))) { + result.push({ + id: SyncResource.Profiles, + label: getSyncAreaLabel(SyncResource.Profiles), + }); + } + return result; } private updateConfiguration(items: ConfigureSyncQuickPickItem[], selectedItems: ReadonlyArray): void { @@ -744,33 +753,35 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo const turnOnSyncWhenContext = ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT.toNegated(), CONTEXT_ACCOUNT_STATE.notEqualsTo(AccountStatus.Uninitialized), CONTEXT_TURNING_ON_STATE.negate()); CommandsRegistry.registerCommand(turnOnSyncCommand.id, () => this.turnOn()); MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { - group: '5_sync', + group: '5_settings', command: { id: turnOnSyncCommand.id, title: localize('global activity turn on sync', "Turn on Settings Sync...") }, when: ContextKeyExpr.and(turnOnSyncWhenContext, CONTEXT_SYNC_AFTER_INITIALIZATION.negate()), - order: 1 + order: 3 }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: turnOnSyncCommand, when: turnOnSyncWhenContext, }); MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { - group: '5_sync', + group: '5_settings', command: { id: turnOnSyncCommand.id, title: localize('global activity turn on sync', "Turn on Settings Sync...") }, when: turnOnSyncWhenContext, + order: 3 }); MenuRegistry.appendMenuItem(MenuId.AccountsContext, { - group: '1_sync', + group: '1_settings', command: { id: turnOnSyncCommand.id, title: localize('global activity turn on sync', "Turn on Settings Sync...") }, - when: turnOnSyncWhenContext + when: turnOnSyncWhenContext, + order: 2 }); } @@ -784,10 +795,10 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo id, title: localize('ask to turn on in global', "Settings Sync is Off (1)"), menu: { - group: '5_sync', + group: '5_settings', id: MenuId.GlobalActivity, when, - order: 2 + order: 3 } }); } @@ -810,12 +821,12 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo title: localize('turnin on sync', "Turning on Settings Sync..."), precondition: ContextKeyExpr.false(), menu: [{ - group: '5_sync', + group: '5_settings', id: MenuId.GlobalActivity, when, - order: 2 + order: 3 }, { - group: '1_sync', + group: '1_settings', id: MenuId.AccountsContext, when, }] @@ -835,7 +846,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo icon: Codicon.stopCircle, menu: { id: MenuId.ViewContainerTitle, - when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT.toNegated(), CONTEXT_ACCOUNT_STATE.notEqualsTo(AccountStatus.Uninitialized), CONTEXT_TURNING_ON_STATE, ContextKeyExpr.equals('viewContainer', SYNC_VIEW_CONTAINER_ID)), + when: ContextKeyExpr.and(CONTEXT_TURNING_ON_STATE, ContextKeyExpr.equals('viewContainer', SYNC_VIEW_CONTAINER_ID)), group: 'navigation', order: 1 } @@ -857,10 +868,10 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo id: 'workbench.userData.actions.signin', title: localize('sign in global', "Sign in to Sync Settings"), menu: { - group: '5_sync', + group: '5_settings', id: MenuId.GlobalActivity, when, - order: 2 + order: 3 } }); } @@ -873,7 +884,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } })); this._register(MenuRegistry.appendMenuItem(MenuId.AccountsContext, { - group: '1_sync', + group: '1_settings', command: { id, title: localize('sign in accounts', "Sign in to Sync Settings (1)"), @@ -886,7 +897,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo CommandsRegistry.registerCommand(showConflictsCommand.id, () => this.userDataSyncWorkbenchService.showConflicts()); const getTitle = () => localize('resolveConflicts_global', "{0}: Show Conflicts ({1})", SYNC_TITLE, this.getConflictsCount()); MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { - group: '5_sync', + group: '5_settings', command: { id: showConflictsCommand.id, get title() { return getTitle(); } @@ -895,7 +906,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo order: 2 }); MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { - group: '5_sync', + group: '5_settings', command: { id: showConflictsCommand.id, get title() { return getTitle(); } @@ -920,19 +931,19 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo menu: [ { id: MenuId.GlobalActivity, - group: '5_sync', + group: '5_settings', when, order: 3 }, { id: MenuId.MenubarPreferencesMenu, - group: '5_sync', + group: '5_settings', when, order: 3, }, { id: MenuId.AccountsContext, - group: '1_sync', + group: '1_settings', when, } ], @@ -1057,7 +1068,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo when }, { id: MenuId.ViewContainerTitle, - when: ContextKeyExpr.and(when, ContextKeyExpr.equals('viewContainer', SYNC_VIEW_CONTAINER_ID)), + when: ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, ContextKeyExpr.equals('viewContainer', SYNC_VIEW_CONTAINER_ID)), group: 'navigation', order: 2 }] diff --git a/src/vs/workbench/contrib/webview/browser/overlayWebview.ts b/src/vs/workbench/contrib/webview/browser/overlayWebview.ts index 3eeb445b292..24aeee9cfa7 100644 --- a/src/vs/workbench/contrib/webview/browser/overlayWebview.ts +++ b/src/vs/workbench/contrib/webview/browser/overlayWebview.ts @@ -42,6 +42,8 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { private readonly _scopedContextKeyService = this._register(new MutableDisposable()); private _findWidgetVisible: IContextKey | undefined; private _findWidgetEnabled: IContextKey | undefined; + // This isn't associated with an editor action so doesn't need to be a context key + private _findActiveWhenHidden: boolean | undefined = false; public readonly id: string; public readonly providedViewType?: string; @@ -147,7 +149,11 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { if (this._container) { this._container.style.visibility = 'hidden'; } - if (!this._options.retainContextWhenHidden) { + if (this._options.retainContextWhenHidden) { + // https://github.com/microsoft/vscode/issues/157424 + // We need to record the current state when retaining context so we can try to showFind() when showing webview again + this.hideFind(); + } else { this._webview.clear(); this._webviewEvents.clear(); } @@ -207,6 +213,9 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { this._webview.value.setContextKeyService(this._scopedContextKeyService.value); } + // https://github.com/microsoft/vscode/issues/157424 + this.tryShowFind(); + if (this._html) { webview.html = this._html; } @@ -336,6 +345,20 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { undo(): void { this._webview.value?.undo(); } redo(): void { this._webview.value?.redo(); } + /** + * Only meant to be used when we're showing webview as an attempt to reload the previously hidden + * find widget when retaining context + */ + tryShowFind() { + const shouldShowFind: boolean | undefined = ( + (this.options.retainContextWhenHidden && this._findActiveWhenHidden) + ); + + if (shouldShowFind) { + this.showFind(); + } + } + showFind() { if (this._webview.value) { this._webview.value.showFind(); @@ -344,6 +367,7 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { } hideFind() { + this._findActiveWhenHidden = this._findWidgetVisible?.get(); this._findWidgetVisible?.reset(); this._webview.value?.hideFind(); } diff --git a/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html b/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html index f070ed0bb60..cd4afd479a5 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html +++ b/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html @@ -332,7 +332,7 @@ if (!crypto.subtle) { // cannot validate, not running in a secure context - throw new Error(`Cannot validate in current context!`); + throw new Error(`'crypto.subtle' is not available so webviews will not work. This is likely because the editor is not running in a secure context (https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts).`); } // Here the `parentOriginHash()` function from `src/vs/workbench/common/webview.ts` is inlined @@ -432,7 +432,10 @@ activeTheme: undefined, /** @type {string | undefined} */ - themeName: undefined, + themeId: undefined, + + /** @type {string | undefined} */ + themeLabel: undefined, /** @type {boolean} */ screenReader: false, @@ -490,7 +493,9 @@ } body.dataset.vscodeThemeKind = initData.activeTheme; - body.dataset.vscodeThemeName = initData.themeName || ''; + /** @deprecated data-vscode-theme-name will be removed, use data-vscode-theme-id instead */ + body.dataset.vscodeThemeName = initData.themeLabel || ''; + body.dataset.vscodeThemeId = initData.themeId || ''; } if (initData.styles) { @@ -820,7 +825,8 @@ initData.styles = data.styles; initData.activeTheme = data.activeTheme; - initData.themeName = data.themeName; + initData.themeLabel = data.themeLabel; + initData.themeId = data.themeId; initData.reduceMotion = data.reduceMotion; initData.screenReader = data.screenReader; diff --git a/src/vs/workbench/contrib/webview/browser/pre/index.html b/src/vs/workbench/contrib/webview/browser/pre/index.html index 7053737aaba..6c90f4d295a 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/index.html +++ b/src/vs/workbench/contrib/webview/browser/pre/index.html @@ -5,7 +5,7 @@ + content="default-src 'none'; script-src 'sha256-wwaDxsm1+SKIUb5YJXiZlYMyV7QPB8+zd6HPcTjigZs=' 'self'; frame-src 'self'; style-src 'unsafe-inline';"> ; } @@ -27,7 +28,7 @@ export class WebviewThemeDataProvider extends Disposable { public readonly onThemeDataChanged = this._onThemeDataChanged.event; constructor( - @IThemeService private readonly _themeService: IThemeService, + @IWorkbenchThemeService private readonly _themeService: IWorkbenchThemeService, @IConfigurationService private readonly _configurationService: IConfigurationService, ) { super(); @@ -44,7 +45,7 @@ export class WebviewThemeDataProvider extends Disposable { })); } - public getTheme(): IColorTheme { + public getTheme(): IWorkbenchColorTheme { return this._themeService.getColorTheme(); } @@ -75,7 +76,7 @@ export class WebviewThemeDataProvider extends Disposable { }; const activeTheme = ApiThemeClassName.fromTheme(theme); - this._cachedWebViewThemeData = { styles, activeTheme, themeLabel: theme.label, }; + this._cachedWebViewThemeData = { styles, activeTheme, themeLabel: theme.label, themeId: theme.settingsId }; } return this._cachedWebViewThemeData; @@ -95,7 +96,7 @@ enum ApiThemeClassName { } namespace ApiThemeClassName { - export function fromTheme(theme: IColorTheme): ApiThemeClassName { + export function fromTheme(theme: IWorkbenchColorTheme): ApiThemeClassName { switch (theme.type) { case ColorScheme.LIGHT: return ApiThemeClassName.light; case ColorScheme.DARK: return ApiThemeClassName.dark; diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index 90948ab4008..88266a40eeb 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -737,7 +737,7 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD } protected style(): void { - let { styles, activeTheme, themeLabel } = this.webviewThemeDataProvider.getWebviewThemeData(); + let { styles, activeTheme, themeLabel, themeId } = this.webviewThemeDataProvider.getWebviewThemeData(); if (this.options.transformCssVariables) { styles = this.options.transformCssVariables(styles); } @@ -745,7 +745,7 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD const reduceMotion = this._accessibilityService.isMotionReduced(); const screenReader = this._accessibilityService.isScreenReaderOptimized(); - this._send('styles', { styles, activeTheme, themeName: themeLabel, reduceMotion, screenReader }); + this._send('styles', { styles, activeTheme, themeId, themeLabel, reduceMotion, screenReader }); this.styledFindWidget(); } diff --git a/src/vs/workbench/electron-sandbox/parts/titlebar/menubarControl.ts b/src/vs/workbench/electron-sandbox/parts/titlebar/menubarControl.ts index 813613e9b80..b9aa17ff080 100644 --- a/src/vs/workbench/electron-sandbox/parts/titlebar/menubarControl.ts +++ b/src/vs/workbench/electron-sandbox/parts/titlebar/menubarControl.ts @@ -120,8 +120,8 @@ export class NativeMenubarControl extends MenubarControl { const submenu = { items: [] }; if (!this.menus[menuItem.item.submenu.id]) { - const menu = this.menus[menuItem.item.submenu.id] = this._register(this.menuService.createMenu(menuItem.item.submenu, this.contextKeyService)); - this._register(menu.onDidChange(() => this.updateMenubar())); + const menu = this.menus[menuItem.item.submenu.id] = this.mainMenuDisposables.add(this.menuService.createMenu(menuItem.item.submenu, this.contextKeyService)); + this.mainMenuDisposables.add(menu.onDidChange(() => this.updateMenubar())); } const menuToDispose = this.menuService.createMenu(menuItem.item.submenu, this.contextKeyService); diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index 3a558cc6b01..9a11377d30d 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -65,6 +65,9 @@ import { toErrorMessage } from 'vs/base/common/errorMessage'; import { registerWindowDriver } from 'vs/platform/driver/electron-sandbox/driver'; import { ILabelService } from 'vs/platform/label/common/label'; import { dirname } from 'vs/base/common/resources'; +import { IBannerService } from 'vs/workbench/services/banner/browser/bannerService'; +import { Codicon } from 'vs/base/common/codicons'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; export class NativeWindow extends Disposable { @@ -115,7 +118,9 @@ export class NativeWindow extends Disposable { @IInstantiationService private readonly instantiationService: IInstantiationService, @ISharedProcessService private readonly sharedProcessService: ISharedProcessService, @IProgressService private readonly progressService: IProgressService, - @ILabelService private readonly labelService: ILabelService + @ILabelService private readonly labelService: ILabelService, + @IBannerService private readonly bannerService: IBannerService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService ) { super(); @@ -620,49 +625,20 @@ export class NativeWindow extends Disposable { this.lifecycleService.when(LifecyclePhase.Ready).then(() => this.nativeHostService.notifyReady()); this.lifecycleService.when(LifecyclePhase.Restored).then(() => this.sharedProcessService.notifyRestored()); - // Integrity warning - this.integrityService.isPure().then(({ isPure }) => this.titleService.updateProperties({ isPure })); - - // Root warning - this.lifecycleService.when(LifecyclePhase.Restored).then(async () => { - const isAdmin = await this.nativeHostService.isAdmin(); - - // Update title - this.titleService.updateProperties({ isAdmin }); - - // Show warning message (unix only) - if (isAdmin && !isWindows) { - this.notificationService.warn(localize('runningAsRoot', "It is not recommended to run {0} as root user.", this.productService.nameShort)); - } - }); - - // Windows 7 warning - if (isWindows) { - this.lifecycleService.when(LifecyclePhase.Restored).then(async () => { - const version = this.environmentService.os.release.split('.'); - - // Refs https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-osversioninfoa - if (parseInt(version[0]) === 6 && parseInt(version[1]) === 1) { - const message = localize('windows 7 eol', "{0} on Windows 7 will no longer receive any further updates.", this.productService.nameLong); - - this.notificationService.prompt( - Severity.Warning, - message, - [{ - label: localize('learnMore', "Learn More"), - run: () => this.openerService.open(URI.parse('https://aka.ms/vscode-faq-win7')) - }], - { - neverShowAgain: { id: 'windows7eol', isSecondary: true, scope: NeverShowAgainScope.APPLICATION } - } - ); - } - }); - } + // Check for situations that are worth warning the user about + this.handleWarnings(); // Touchbar menu (if enabled) this.updateTouchbarMenu(); + // Smoke Test Driver + if (this.environmentService.enableSmokeTestDriver) { + this.setupDriver(); + } + } + + private async handleWarnings(): Promise { + // Check for cyclic dependencies if (require.hasDependencyCycle()) { if (isCI) { @@ -674,9 +650,59 @@ export class NativeWindow extends Disposable { } } - // Smoke Test Driver - if (this.environmentService.enableSmokeTestDriver) { - this.setupDriver(); + // After restored phase is fine for the following ones + await this.lifecycleService.when(LifecyclePhase.Restored); + + // Integrity / Root warning + (async () => { + const isAdmin = await this.nativeHostService.isAdmin(); + const { isPure } = await this.integrityService.isPure(); + + // Update to title + this.titleService.updateProperties({ isPure, isAdmin }); + + // Show warning message (unix only) + if (isAdmin && !isWindows) { + this.notificationService.warn(localize('runningAsRoot', "It is not recommended to run {0} as root user.", this.productService.nameShort)); + } + })(); + + // Installation Dir Warning + if (this.environmentService.isBuilt) { + const installLocationUri = URI.file(this.environmentService.appRoot); + for (const folder of this.contextService.getWorkspace().folders) { + if (this.uriIdentityService.extUri.isEqualOrParent(folder.uri, installLocationUri)) { + this.bannerService.show({ + id: 'appRootWarning.banner', + message: localize('appRootWarning.banner', "Files you store within the installation folder ('{0}') may be OVERWRITTEN or DELETED IRREVERSIBLY without warning at update time.", this.environmentService.appRoot), + icon: Codicon.warning + }); + + break; + } + } + } + + // Windows 7 warning + if (isWindows) { + const version = this.environmentService.os.release.split('.'); + + // Refs https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-osversioninfoa + if (parseInt(version[0]) === 6 && parseInt(version[1]) === 1) { + const message = localize('windows 7 eol', "{0} on Windows 7 will no longer receive any further updates.", this.productService.nameLong); + + this.notificationService.prompt( + Severity.Warning, + message, + [{ + label: localize('learnMore', "Learn More"), + run: () => this.openerService.open(URI.parse('https://aka.ms/vscode-faq-win7')) + }], + { + neverShowAgain: { id: 'windows7eol', isSecondary: true, scope: NeverShowAgainScope.APPLICATION } + } + ); + } } } @@ -860,7 +886,7 @@ export class NativeWindow extends Disposable { const diffMode = !!(request.filesToDiff && (request.filesToDiff.length === 2)); const mergeMode = !!(request.filesToMerge && (request.filesToMerge.length === 4)); - const inputs = await pathsToEditors(mergeMode ? request.filesToMerge : diffMode ? request.filesToDiff : request.filesToOpenOrCreate, this.fileService); + const inputs = coalesce(await pathsToEditors(mergeMode ? request.filesToMerge : diffMode ? request.filesToDiff : request.filesToOpenOrCreate, this.fileService)); if (inputs.length) { const openedEditorPanes = await this.openResources(inputs, diffMode, mergeMode); diff --git a/src/vs/workbench/services/authentication/browser/authenticationService.ts b/src/vs/workbench/services/authentication/browser/authenticationService.ts index 921cfbcb8ba..69cb0d2ba48 100644 --- a/src/vs/workbench/services/authentication/browser/authenticationService.ts +++ b/src/vs/workbench/services/authentication/browser/authenticationService.ts @@ -16,7 +16,7 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { Severity } from 'vs/platform/notification/common/notification'; import { IProductService } from 'vs/platform/product/common/productService'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; @@ -163,9 +163,16 @@ const authenticationExtPoint = ExtensionsRegistry.registerExtensionPoint(); private _sessionAccessRequestItems = new Map(); private _accountBadgeDisposable = this._register(new MutableDisposable()); @@ -198,13 +205,6 @@ export class AuthenticationService extends Disposable implements IAuthentication @IQuickInputService private readonly quickInputService: IQuickInputService ) { super(); - this._placeholderMenuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { - command: { - id: 'noAuthenticationProviders', - title: nls.localize('authentication.Placeholder', "No accounts requested yet..."), - precondition: ContextKeyExpr.false() - }, - }); authenticationExtPoint.setHandler((extensions, { added, removed }) => { added.forEach(point => { @@ -255,9 +255,9 @@ export class AuthenticationService extends Disposable implements IAuthentication this._authenticationProviders.set(id, authenticationProvider); this._onDidRegisterAuthenticationProvider.fire({ id, label: authenticationProvider.label }); - if (this._placeholderMenuItem) { - this._placeholderMenuItem.dispose(); - this._placeholderMenuItem = undefined; + if (placeholderMenuItem) { + placeholderMenuItem.dispose(); + placeholderMenuItem = undefined; } } @@ -275,7 +275,7 @@ export class AuthenticationService extends Disposable implements IAuthentication } if (!this._authenticationProviders.size) { - this._placeholderMenuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { + placeholderMenuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { command: { id: 'noAuthenticationProviders', title: nls.localize('loading', "Loading..."), @@ -735,4 +735,4 @@ export class AuthenticationService extends Disposable implements IAuthentication } } -registerSingleton(IAuthenticationService, AuthenticationService, false); +registerSingleton(IAuthenticationService, AuthenticationService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts b/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts index 8d694700fd9..b09c88322f5 100644 --- a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts @@ -1072,6 +1072,116 @@ suite('WorkspaceConfigurationService - Folder', () => { assert.strictEqual(actual.workspaceValue, 'workspaceValue'); assert.strictEqual(actual.workspaceFolderValue, undefined); assert.strictEqual(actual.value, 'workspaceValue'); + + await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'tasks.json'), VSBuffer.fromString('{ "configurationService.tasks.testSetting": "tasksValue" }')); + await testObject.reloadConfiguration(); + actual = testObject.inspect('tasks'); + assert.strictEqual(actual.defaultValue, undefined); + assert.strictEqual(actual.application, undefined); + assert.deepStrictEqual(actual.userValue, {}); + assert.deepStrictEqual(actual.workspaceValue, { + "configurationService": { + "tasks": { + "testSetting": "tasksValue" + } + } + }); + assert.strictEqual(actual.workspaceFolderValue, undefined); + assert.deepStrictEqual(actual.value, { + "configurationService": { + "tasks": { + "testSetting": "tasksValue" + } + } + }); + }); + + test('inspect restricted settings', async () => { + testObject.updateWorkspaceTrust(false); + await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "configurationService.folder.restrictedSetting": "userRestrictedValue" }')); + await testObject.reloadConfiguration(); + let actual = testObject.inspect('configurationService.folder.restrictedSetting'); + assert.strictEqual(actual.defaultValue, 'isSet'); + assert.strictEqual(actual.application, undefined); + assert.strictEqual(actual.userValue, 'userRestrictedValue'); + assert.strictEqual(actual.workspaceValue, undefined); + assert.strictEqual(actual.workspaceFolderValue, undefined); + assert.strictEqual(actual.value, 'userRestrictedValue'); + + testObject.updateWorkspaceTrust(true); + await testObject.reloadConfiguration(); + actual = testObject.inspect('configurationService.folder.restrictedSetting'); + assert.strictEqual(actual.defaultValue, 'isSet'); + assert.strictEqual(actual.application, undefined); + assert.strictEqual(actual.userValue, 'userRestrictedValue'); + assert.strictEqual(actual.workspaceValue, undefined); + assert.strictEqual(actual.workspaceFolderValue, undefined); + assert.strictEqual(actual.value, 'userRestrictedValue'); + + testObject.updateWorkspaceTrust(false); + await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.restrictedSetting": "workspaceRestrictedValue" }')); + await testObject.reloadConfiguration(); + actual = testObject.inspect('configurationService.folder.restrictedSetting'); + assert.strictEqual(actual.defaultValue, 'isSet'); + assert.strictEqual(actual.application, undefined); + assert.strictEqual(actual.userValue, 'userRestrictedValue'); + assert.strictEqual(actual.workspaceValue, 'workspaceRestrictedValue'); + assert.strictEqual(actual.workspaceFolderValue, undefined); + assert.strictEqual(actual.value, 'userRestrictedValue'); + + await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'tasks.json'), VSBuffer.fromString('{ "configurationService.tasks.testSetting": "tasksValue" }')); + await testObject.reloadConfiguration(); + actual = testObject.inspect('tasks'); + assert.strictEqual(actual.defaultValue, undefined); + assert.strictEqual(actual.application, undefined); + assert.deepStrictEqual(actual.userValue, {}); + assert.deepStrictEqual(actual.workspaceValue, { + "configurationService": { + "tasks": { + "testSetting": "tasksValue" + } + } + }); + assert.strictEqual(actual.workspaceFolderValue, undefined); + assert.deepStrictEqual(actual.value, { + "configurationService": { + "tasks": { + "testSetting": "tasksValue" + } + } + }); + + testObject.updateWorkspaceTrust(true); + await testObject.reloadConfiguration(); + actual = testObject.inspect('configurationService.folder.restrictedSetting'); + assert.strictEqual(actual.defaultValue, 'isSet'); + assert.strictEqual(actual.application, undefined); + assert.strictEqual(actual.userValue, 'userRestrictedValue'); + assert.strictEqual(actual.workspaceValue, 'workspaceRestrictedValue'); + assert.strictEqual(actual.workspaceFolderValue, undefined); + assert.strictEqual(actual.value, 'workspaceRestrictedValue'); + + await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'tasks.json'), VSBuffer.fromString('{ "configurationService.tasks.testSetting": "tasksValue" }')); + await testObject.reloadConfiguration(); + actual = testObject.inspect('tasks'); + assert.strictEqual(actual.defaultValue, undefined); + assert.strictEqual(actual.application, undefined); + assert.deepStrictEqual(actual.userValue, {}); + assert.deepStrictEqual(actual.workspaceValue, { + "configurationService": { + "tasks": { + "testSetting": "tasksValue" + } + } + }); + assert.strictEqual(actual.workspaceFolderValue, undefined); + assert.deepStrictEqual(actual.value, { + "configurationService": { + "tasks": { + "testSetting": "tasksValue" + } + } + }); }); test('keys', async () => { @@ -1481,7 +1591,7 @@ suite('WorkspaceConfigurationService - Profiles', () => { actual = testObject.inspect('configurationService.profiles.applicationSetting'); assert.strictEqual(actual.defaultValue, 'isSet'); assert.strictEqual(actual.applicationValue, 'applicationValue'); - assert.strictEqual(actual.userValue, undefined); + assert.strictEqual(actual.userValue, 'profileValue'); assert.strictEqual(actual.workspaceValue, undefined); assert.strictEqual(actual.workspaceFolderValue, undefined); assert.strictEqual(actual.value, 'applicationValue'); @@ -1942,6 +2052,50 @@ suite('WorkspaceConfigurationService-Multiroot', () => { assert.strictEqual(actual.value, 'workspaceFolderValue'); }); + test('inspect restricted settings', async () => { + testObject.updateWorkspaceTrust(false); + await jsonEditingServce.write((workspaceContextService.getWorkspace().configuration!), [{ path: ['settings'], value: { 'configurationService.workspace.testRestrictedSetting1': 'workspaceRestrictedValue' } }], true); + await testObject.reloadConfiguration(); + let actual = testObject.inspect('configurationService.workspace.testRestrictedSetting1', { resource: workspaceContextService.getWorkspace().folders[0].uri }); + assert.strictEqual(actual.defaultValue, 'isSet'); + assert.strictEqual(actual.application, undefined); + assert.strictEqual(actual.userValue, undefined); + assert.strictEqual(actual.workspaceValue, 'workspaceRestrictedValue'); + assert.strictEqual(actual.workspaceFolderValue, undefined); + assert.strictEqual(actual.value, 'isSet'); + + testObject.updateWorkspaceTrust(true); + await testObject.reloadConfiguration(); + actual = testObject.inspect('configurationService.workspace.testRestrictedSetting1', { resource: workspaceContextService.getWorkspace().folders[0].uri }); + assert.strictEqual(actual.defaultValue, 'isSet'); + assert.strictEqual(actual.application, undefined); + assert.strictEqual(actual.userValue, undefined); + assert.strictEqual(actual.workspaceValue, 'workspaceRestrictedValue'); + assert.strictEqual(actual.workspaceFolderValue, undefined); + assert.strictEqual(actual.value, 'workspaceRestrictedValue'); + + testObject.updateWorkspaceTrust(false); + await fileService.writeFile(workspaceContextService.getWorkspace().folders[0].toResource('.vscode/settings.json'), VSBuffer.fromString('{ "configurationService.workspace.testRestrictedSetting1": "workspaceFolderRestrictedValue" }')); + await testObject.reloadConfiguration(); + actual = testObject.inspect('configurationService.workspace.testRestrictedSetting1', { resource: workspaceContextService.getWorkspace().folders[0].uri }); + assert.strictEqual(actual.defaultValue, 'isSet'); + assert.strictEqual(actual.application, undefined); + assert.strictEqual(actual.userValue, undefined); + assert.strictEqual(actual.workspaceValue, 'workspaceRestrictedValue'); + assert.strictEqual(actual.workspaceFolderValue, 'workspaceFolderRestrictedValue'); + assert.strictEqual(actual.value, 'isSet'); + + testObject.updateWorkspaceTrust(true); + await testObject.reloadConfiguration(); + actual = testObject.inspect('configurationService.workspace.testRestrictedSetting1', { resource: workspaceContextService.getWorkspace().folders[0].uri }); + assert.strictEqual(actual.defaultValue, 'isSet'); + assert.strictEqual(actual.application, undefined); + assert.strictEqual(actual.userValue, undefined); + assert.strictEqual(actual.workspaceValue, 'workspaceRestrictedValue'); + assert.strictEqual(actual.workspaceFolderValue, 'workspaceFolderRestrictedValue'); + assert.strictEqual(actual.value, 'workspaceFolderRestrictedValue'); + }); + test('get launch configuration', async () => { const expectedLaunchConfiguration = { 'version': '0.1.0', diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index 2e19d0d0cbf..5ceeaa56b7e 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -62,12 +62,32 @@ export const enum GroupsArrangement { } export interface GroupLayoutArgument { + + /** + * Only applies when there are multiple groups + * arranged next to each other in a row or column. + * If provided, their sum must be 1 to be applied + * per row or column. + */ size?: number; + + /** + * Editor groups will be laid out orthogonal to the + * parent orientation. + */ groups?: GroupLayoutArgument[]; } export interface EditorGroupLayout { + + /** + * The initial orientation of the editor groups at the root. + */ orientation: GroupOrientation; + + /** + * The editor groups at the root of the layout. + */ groups: GroupLayoutArgument[]; } diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index c008162ce50..996411b02fa 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -138,8 +138,8 @@ export abstract class AbstractExtensionService extends Disposable implements IEx private readonly _onDidChangeExtensionsStatus: Emitter = this._register(new Emitter()); public readonly onDidChangeExtensionsStatus: Event = this._onDidChangeExtensionsStatus.event; - private readonly _onDidChangeExtensions: Emitter = this._register(new Emitter({ leakWarningThreshold: 400 })); - public readonly onDidChangeExtensions: Event = this._onDidChangeExtensions.event; + private readonly _onDidChangeExtensions = this._register(new Emitter<{ readonly added: ReadonlyArray; readonly removed: ReadonlyArray }>({ leakWarningThreshold: 400 })); + public readonly onDidChangeExtensions = this._onDidChangeExtensions.event; private readonly _onWillActivateByEvent = this._register(new Emitter()); public readonly onWillActivateByEvent: Event = this._onWillActivateByEvent.event; @@ -596,7 +596,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx // Update the local registry const result = this._registry.deltaExtensions(toAdd, toRemove.map(e => e.identifier)); - this._onDidChangeExtensions.fire(undefined); + this._onDidChangeExtensions.fire({ added: toAdd, removed: toRemove }); toRemove = toRemove.concat(result.removedDueToLooping); if (result.removedDueToLooping.length > 0) { @@ -1049,10 +1049,12 @@ export abstract class AbstractExtensionService extends Disposable implements IEx return this._installedExtensionsReady.wait(); } - public getExtensions(): Promise { - return this._installedExtensionsReady.wait().then(() => { - return this._registry.getAllExtensionDescriptions(); - }); + get extensions(): IExtensionDescription[] { + return this._registry.getAllExtensionDescriptions(); + } + + protected getExtensions(): Promise { + return this._installedExtensionsReady.wait().then(() => this.extensions); } public getExtension(id: string): Promise { diff --git a/src/vs/workbench/services/extensions/common/extensions.ts b/src/vs/workbench/services/extensions/common/extensions.ts index c3e783265a1..269ad124f08 100644 --- a/src/vs/workbench/services/extensions/common/extensions.ts +++ b/src/vs/workbench/services/extensions/common/extensions.ts @@ -443,7 +443,15 @@ export interface IExtensionService { /** * Fired when the available extensions change (i.e. when extensions are added or removed). */ - onDidChangeExtensions: Event; + onDidChangeExtensions: Event<{ readonly added: readonly IExtensionDescription[]; readonly removed: readonly IExtensionDescription[] }>; + + /** + * All registered extensions. + * - List will be empty initially during workbench startup and will be filled with extensions as they are registered + * - Listen to `onDidChangeExtensions` event for any changes to the extensions list. It will change as extensions get registered or de-reigstered. + * - Listen to `onDidRegisterExtensions` event or wait for `whenInstalledExtensionsRegistered` promise to get the initial list of registered extensions. + */ + readonly extensions: readonly IExtensionDescription[]; /** * An event that is fired when activation happens. @@ -481,11 +489,6 @@ export interface IExtensionService { */ whenInstalledExtensionsRegistered(): Promise; - /** - * Return all registered extensions - */ - getExtensions(): Promise; - /** * Return a specific extension * @param id An extension id @@ -597,13 +600,13 @@ export class NullExtensionService implements IExtensionService { declare readonly _serviceBrand: undefined; onDidRegisterExtensions: Event = Event.None; onDidChangeExtensionsStatus: Event = Event.None; - onDidChangeExtensions: Event = Event.None; + onDidChangeExtensions = Event.None; onWillActivateByEvent: Event = Event.None; onDidChangeResponsiveChange: Event = Event.None; + readonly extensions = []; activateByEvent(_activationEvent: string): Promise { return Promise.resolve(undefined); } activationEventIsDone(_activationEvent: string): boolean { return false; } whenInstalledExtensionsRegistered(): Promise { return Promise.resolve(true); } - getExtensions(): Promise { return Promise.resolve([]); } getExtension() { return Promise.resolve(undefined); } readExtensionPointContributions(_extPoint: IExtensionPoint): Promise[]> { return Promise.resolve(Object.create(null)); } getExtensionsStatus(): { [id: string]: IExtensionsStatus } { return Object.create(null); } diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 42255af3d4e..280b0f3fef6 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -41,6 +41,7 @@ export const allApiProposals = Object.freeze({ localization: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.localization.d.ts', notebookCellExecutionState: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookCellExecutionState.d.ts', notebookContentProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookContentProvider.d.ts', + notebookControllerAffinityHidden: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookControllerAffinityHidden.d.ts', notebookControllerKind: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookControllerKind.d.ts', notebookDebugOptions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookDebugOptions.d.ts', notebookDeprecated: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookDeprecated.d.ts', diff --git a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts index 0d9ef9fa9d1..f58e0c58463 100644 --- a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts @@ -550,6 +550,16 @@ export const schema: IJSONSchema = { icon: { type: 'string', description: nls.localize('vscode.extension.icon', 'The path to a 128x128 pixel icon.') + }, + l10n: { + type: 'string', + description: nls.localize({ + key: 'vscode.extension.l10n', + comment: [ + '{Locked="bundle.l10n._locale_.json"}', + '{Locked="vscode.l10n API"}' + ] + }, 'The relative path to a folder containing localization (bundle.l10n.*.json) files. Must be specified if you are using the vscode.l10n API.') } } }; diff --git a/src/vs/workbench/services/extensions/electron-sandbox/extensionHostProfiler.ts b/src/vs/workbench/services/extensions/electron-sandbox/extensionHostProfiler.ts index f2a029bf0a2..4b63dbd1d4e 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/extensionHostProfiler.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/extensionHostProfiler.ts @@ -28,13 +28,14 @@ export class ExtensionHostProfiler { return { stop: once(async () => { const profile = await this._profilingService.stopProfiling(id); - const extensions = await this._extensionService.getExtensions(); + await this._extensionService.whenInstalledExtensionsRegistered(); + const extensions = this._extensionService.extensions; return this._distill(profile, extensions); }) }; } - private _distill(profile: IV8Profile, extensions: IExtensionDescription[]): IExtensionHostProfile { + private _distill(profile: IV8Profile, extensions: readonly IExtensionDescription[]): IExtensionHostProfile { const searchTree = TernarySearchTree.forUris(); for (const extension of extensions) { if (extension.extensionLocation.scheme === Schemas.file) { diff --git a/src/vs/workbench/services/host/browser/browserHostService.ts b/src/vs/workbench/services/host/browser/browserHostService.ts index efba94ca42a..63ddc762602 100644 --- a/src/vs/workbench/services/host/browser/browserHostService.ts +++ b/src/vs/workbench/services/host/browser/browserHostService.ts @@ -36,6 +36,7 @@ import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { Schemas } from 'vs/base/common/network'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { coalesce } from 'vs/base/common/arrays'; /** * A workspace to open in the workbench can either be: @@ -258,7 +259,7 @@ export class BrowserHostService extends Disposable implements IHostService { // Support mergeMode if (options?.mergeMode && fileOpenables.length === 4) { - const editors = await pathsToEditors(fileOpenables, this.fileService); + const editors = coalesce(await pathsToEditors(fileOpenables, this.fileService)); if (editors.length !== 4 || !isResourceEditorInput(editors[0]) || !isResourceEditorInput(editors[1]) || !isResourceEditorInput(editors[2]) || !isResourceEditorInput(editors[3])) { return; // invalid resources } @@ -288,7 +289,7 @@ export class BrowserHostService extends Disposable implements IHostService { // Support diffMode if (options?.diffMode && fileOpenables.length === 2) { - const editors = await pathsToEditors(fileOpenables, this.fileService); + const editors = coalesce(await pathsToEditors(fileOpenables, this.fileService)); if (editors.length !== 2 || !isResourceEditorInput(editors[0]) || !isResourceEditorInput(editors[1])) { return; // invalid resources } @@ -333,7 +334,7 @@ export class BrowserHostService extends Disposable implements IHostService { openables = [openable]; } - editorService.openEditors(await pathsToEditors(openables, this.fileService), undefined, { validateTrust: true }); + editorService.openEditors(coalesce(await pathsToEditors(openables, this.fileService)), undefined, { validateTrust: true }); } // New Window: open into empty window diff --git a/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts b/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts index 1b50fc3011e..1b3db8dbbfd 100644 --- a/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts +++ b/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts @@ -11,7 +11,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { ILanguageService } from 'vs/editor/common/languages/language'; import { URI } from 'vs/base/common/uri'; import { isWeb } from 'vs/base/common/platform'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { LanguageDetectionSimpleWorker } from 'vs/workbench/services/languageDetection/browser/languageDetectionSimpleWorker'; import { IModelService } from 'vs/editor/common/services/model'; import { SimpleWorkerClient } from 'vs/base/common/worker/simpleWorker'; @@ -149,6 +149,9 @@ export class LanguageDetectionService extends Disposable implements ILanguageDet return this._languageDetectionWorkerClient.detectLanguage(resource, biases, preferHistory, supportedLangs); } + // TODO: explore using the history service or something similar to provide this list of opened editors + // so this service can support delayed instantiation. This may be tricky since it seems the IHistoryService + // only gives history for a workspace... where this takes advantage of history at a global level as well. private initEditorOpenedListeners(storageService: IStorageService) { try { const globalLangHistroyData = JSON.parse(storageService.get(LanguageDetectionService.globalOpenedLanguagesStorageKey, StorageScope.PROFILE, '[]')); @@ -354,4 +357,5 @@ export class LanguageDetectionWorkerClient extends EditorWorkerClient { } } -registerSingleton(ILanguageDetectionService, LanguageDetectionService, false); +// For now we use Eager until we handle keeping track of history better. +registerSingleton(ILanguageDetectionService, LanguageDetectionService, InstantiationType.Eager); diff --git a/src/vs/workbench/services/search/common/search.ts b/src/vs/workbench/services/search/common/search.ts index 9377cbfc1e4..2ed83f3be21 100644 --- a/src/vs/workbench/services/search/common/search.ts +++ b/src/vs/workbench/services/search/common/search.ts @@ -348,6 +348,11 @@ export class OneLineRange extends SearchRange { } } +export const enum ViewMode { + List = 'list', + Tree = 'tree' +} + export const enum SearchSortOrder { Default = 'default', FileNames = 'fileNames', @@ -393,6 +398,7 @@ export interface ISearchConfigurationProperties { colors: boolean; badges: boolean; }; + defaultViewMode: ViewMode; } export interface ISearchConfiguration extends IFilesConfiguration { diff --git a/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts b/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts index 9ab643b1284..d5eaafc1d19 100644 --- a/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts +++ b/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts @@ -301,11 +301,15 @@ export class RipgrepParser extends EventEmitter { } const matchText = bytesOrTextToString(match.match); - const inBetweenChars = fullTextBytes.slice(prevMatchEnd, match.start).toString().length; - const startCol = prevMatchEndCol + inBetweenChars; + + const inBetweenText = fullTextBytes.slice(prevMatchEnd, match.start).toString(); + const inBetweenStats = getNumLinesAndLastNewlineLength(inBetweenText); + const startCol = inBetweenStats.numLines > 0 ? + inBetweenStats.lastLineLength : + inBetweenStats.lastLineLength + prevMatchEndCol; const stats = getNumLinesAndLastNewlineLength(matchText); - const startLineNumber = prevMatchEndLine; + const startLineNumber = inBetweenStats.numLines + prevMatchEndLine; const endLineNumber = stats.numLines + startLineNumber; const endCol = stats.numLines > 0 ? stats.lastLineLength : diff --git a/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngineUtils.test.ts b/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngineUtils.test.ts index b29c81f2d31..5090c39fc39 100644 --- a/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngineUtils.test.ts +++ b/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngineUtils.test.ts @@ -261,5 +261,39 @@ suite('RipgrepTextSearchEngine', () => { } ]); }); + + test('multiple submatches without newline in between (#131507)', () => { + testParser( + [ + makeRgMatch('file1.js', 'foobarbazquux', 4, [{ start: 0, end: 4 }, { start: 6, end: 10 }]), + ], + [ + { + preview: { + text: 'foobarbazquux', + matches: [new Range(0, 0, 0, 4), new Range(0, 6, 0, 10)] + }, + uri: joinPath(TEST_FOLDER, 'file1.js'), + ranges: [new Range(3, 0, 3, 4), new Range(3, 6, 3, 10)] + } + ]); + }); + + test('multiple submatches with newline in between (#131507)', () => { + testParser( + [ + makeRgMatch('file1.js', 'foo\nbar\nbaz\nquux', 4, [{ start: 0, end: 5 }, { start: 8, end: 13 }]), + ], + [ + { + preview: { + text: 'foo\nbar\nbaz\nquux', + matches: [new Range(0, 0, 1, 1), new Range(2, 0, 3, 1)] + }, + uri: joinPath(TEST_FOLDER, 'file1.js'), + ranges: [new Range(3, 0, 4, 1), new Range(5, 0, 6, 1)] + } + ]); + }); }); }); diff --git a/src/vs/workbench/services/storage/test/browser/storageService.test.ts b/src/vs/workbench/services/storage/test/browser/storageService.test.ts index cbe4837bc8a..483ff21dbea 100644 --- a/src/vs/workbench/services/storage/test/browser/storageService.test.ts +++ b/src/vs/workbench/services/storage/test/browser/storageService.test.ts @@ -37,6 +37,7 @@ async function createStorageService(): Promise<[DisposableStore, BrowserStorageS const inMemoryExtraProfile: IUserDataProfile = { id: 'id', name: 'inMemory', + shortName: 'inMemory', isDefault: false, location: inMemoryExtraProfileRoot, globalStorageHome: joinPath(inMemoryExtraProfileRoot, 'globalStorageHome'), diff --git a/src/vs/workbench/services/userData/browser/userDataInit.ts b/src/vs/workbench/services/userData/browser/userDataInit.ts index 6066266d93d..67ab3ee9976 100644 --- a/src/vs/workbench/services/userData/browser/userDataInit.ts +++ b/src/vs/workbench/services/userData/browser/userDataInit.ts @@ -419,7 +419,8 @@ class NewExtensionsInitializer implements IUserDataInitializer { } private async areExtensionsRunning(extensions: ILocalExtension[]): Promise { - const runningExtensions = await this.extensionService.getExtensions(); + await this.extensionService.whenInstalledExtensionsRegistered(); + const runningExtensions = this.extensionService.extensions; return extensions.every(e => runningExtensions.some(r => areSameExtensions({ id: r.identifier.value }, e.identifier))); } } diff --git a/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts b/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts index 1e8af9e7fb7..f61ec65e803 100644 --- a/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts +++ b/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts @@ -7,7 +7,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { DidChangeProfilesEvent, IUserDataProfile, IUserDataProfilesService, UseDefaultProfileFlags, WorkspaceIdentifier } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { DidChangeProfilesEvent, IUserDataProfile, IUserDataProfileOptions, IUserDataProfilesService, IUserDataProfileUpdateOptions, WorkspaceIdentifier } from 'vs/platform/userDataProfile/common/userDataProfile'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -52,8 +52,8 @@ export class UserDataProfileManagementService extends Disposable implements IUse } } - async createAndEnterProfile(name: string, useDefaultFlags?: UseDefaultProfileFlags, fromExisting?: boolean): Promise { - const profile = await this.userDataProfilesService.createNamedProfile(name, useDefaultFlags, this.getWorkspaceIdentifier()); + async createAndEnterProfile(name: string, options?: IUserDataProfileOptions, fromExisting?: boolean): Promise { + const profile = await this.userDataProfilesService.createNamedProfile(name, options, this.getWorkspaceIdentifier()); await this.enterProfile(profile, !!fromExisting); return profile; } @@ -64,14 +64,14 @@ export class UserDataProfileManagementService extends Disposable implements IUse return profile; } - async renameProfile(profile: IUserDataProfile, name: string): Promise { + async updateProfile(profile: IUserDataProfile, updateOptions: IUserDataProfileUpdateOptions): Promise { if (!this.userDataProfilesService.profiles.some(p => p.id === profile.id)) { throw new Error(`Settings profile ${profile.name} does not exist`); } if (profile.isDefault) { throw new Error(localize('cannotRenameDefaultProfile', "Cannot rename the default settings profile")); } - await this.userDataProfilesService.updateProfile(profile, name); + await this.userDataProfilesService.updateProfile(profile, updateOptions); } async removeProfile(profile: IUserDataProfile): Promise { diff --git a/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts b/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts index 58d36933d16..f45c89a4111 100644 --- a/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts +++ b/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts @@ -8,7 +8,7 @@ import { Event } from 'vs/base/common/event'; import { localize } from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IUserDataProfile, PROFILES_ENABLEMENT_CONFIG, UseDefaultProfileFlags } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IUserDataProfile, IUserDataProfileOptions, IUserDataProfileUpdateOptions, PROFILES_ENABLEMENT_CONFIG } from 'vs/platform/userDataProfile/common/userDataProfile'; import { ContextKeyDefinedExpr, ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ProductQualityContext } from 'vs/platform/contextkey/common/contextkeys'; @@ -32,10 +32,10 @@ export const IUserDataProfileManagementService = createDecorator; + createAndEnterProfile(name: string, options?: IUserDataProfileOptions, fromExisting?: boolean): Promise; createAndEnterTransientProfile(): Promise; removeProfile(profile: IUserDataProfile): Promise; - renameProfile(profile: IUserDataProfile, name: string): Promise; + updateProfile(profile: IUserDataProfile, updateOptions: IUserDataProfileUpdateOptions): Promise; switchProfile(profile: IUserDataProfile): Promise; } @@ -72,10 +72,12 @@ export interface IResourceProfile { } export const ManageProfilesSubMenu = new MenuId('SettingsProfiles'); +export const MANAGE_PROFILES_ACTION_ID = 'workbench.profiles.actions.manage'; export const PROFILES_TTILE = { value: localize('settings profiles', "Settings Profiles"), original: 'Settings Profiles' }; export const PROFILES_CATEGORY = PROFILES_TTILE.value; export const PROFILE_EXTENSION = 'code-profile'; export const PROFILE_FILTER = [{ name: localize('profile', "Settings Profile"), extensions: [PROFILE_EXTENSION] }]; export const PROFILES_ENABLEMENT_CONTEXT = ContextKeyExpr.or(ProductQualityContext.notEqualsTo('stable'), ContextKeyDefinedExpr.create(`config.${PROFILES_ENABLEMENT_CONFIG}`)); export const CURRENT_PROFILE_CONTEXT = new RawContextKey('currentSettingsProfile', ''); +export const IS_CURRENT_PROFILE_TRANSIENT_CONTEXT = new RawContextKey('isCurrentSettingsProfileTransient', false); export const HAS_PROFILES_CONTEXT = new RawContextKey('hasSettingsProfiles', false); diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index 0c6d93f52bf..ac3c3da9155 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -60,6 +60,7 @@ const homeDir = homedir(); const NULL_PROFILE = { name: '', id: '', + shortName: '', isDefault: false, location: URI.file(homeDir), settingsResource: joinPath(URI.file(homeDir), 'settings.json'), @@ -84,7 +85,7 @@ export const TestNativeWindowConfiguration: INativeWindowConfiguration = { product, homeDir: homeDir, tmpDir: tmpdir(), - userDataDir: getUserDataPath(args), + userDataDir: getUserDataPath(args, product.nameShort), profiles: { profile: NULL_PROFILE, all: [NULL_PROFILE] }, ...args }; diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index cef9e9d6638..ec48c866af4 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -178,116 +178,32 @@ import 'vs/workbench/contrib/offline/browser/offline.contribution'; // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! import { create, commands, env, window, workspace, logger } from 'vs/workbench/browser/web.factory'; -import { IWorkbench, ICommand, ICommonTelemetryPropertiesResolver, IDefaultEditor, IDefaultLayout, IDefaultView, IDevelopmentOptions, IExternalUriResolver, IExternalURLOpener, IHomeIndicator, IInitialColorTheme, IPosition, IProductQualityChangeHandler, IRange, IResourceUriProvider, ISettingsSyncOptions, IShowPortCandidate, ITunnel, ITunnelFactory, ITunnelOptions, ITunnelProvider, IWelcomeBanner, IWelcomeBannerAction, IWindowIndicator, IWorkbenchConstructionOptions, Menu } from 'vs/workbench/browser/web.api'; -import { UriComponents, URI } from 'vs/base/common/uri'; -import { IWebSocketFactory, IWebSocket } from 'vs/platform/remote/browser/browserSocketFactory'; +import { Menu } from 'vs/workbench/browser/web.api'; +import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { IProductConfiguration } from 'vs/base/common/product'; -import { ICredentialsProvider } from 'vs/platform/credentials/common/credentials'; -// eslint-disable-next-line no-duplicate-imports -import type { IURLCallbackProvider } from 'vs/workbench/services/url/browser/urlService'; -// eslint-disable-next-line no-duplicate-imports -import type { IUpdateProvider, IUpdate } from 'vs/workbench/services/update/browser/updateService'; -// eslint-disable-next-line no-duplicate-imports -import type { IWorkspace, IWorkspaceProvider } from 'vs/workbench/services/host/browser/browserHostService'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { GroupOrientation } from 'vs/workbench/services/editor/common/editorGroupsService'; export { // Factory create, - IWorkbenchConstructionOptions, - IWorkbench, // Basic Types URI, - UriComponents, Event, Emitter, - IDisposable, Disposable, - - // Workspace - IWorkspace, - IWorkspaceProvider, - - // WebSockets - IWebSocketFactory, - IWebSocket, - - // Resources - IResourceUriProvider, - - // Credentials - ICredentialsProvider, - - // Callbacks - IURLCallbackProvider, - - // LogLevel + GroupOrientation, LogLevel, - // SettingsSync - ISettingsSyncOptions, - - // Updates/Quality - IUpdateProvider, - IUpdate, - IProductQualityChangeHandler, - - // Telemetry - ICommonTelemetryPropertiesResolver, - - // External Uris - IExternalUriResolver, - - // External URL Opener - IExternalURLOpener, - - // Tunnel - ITunnelProvider, - ITunnelFactory, - ITunnel, - ITunnelOptions, - - // Ports - IShowPortCandidate, - - // Commands - ICommand, - commands, - Menu, - - // Logger - logger, - - // Window - window, - - // Branding - IHomeIndicator, - IWelcomeBanner, - IWelcomeBannerAction, - IProductConfiguration, - IWindowIndicator, - IInitialColorTheme, - - // Default layout - IDefaultView, - IDefaultEditor, - IDefaultLayout, - IPosition, - IRange as ISelection, - - // Env + // Facade API env, - - // Workspace + window, workspace, - - // Development - IDevelopmentOptions + commands, + logger, + Menu }; - //#endregion diff --git a/src/vscode-dts/vscode.proposed.extensionLog.d.ts b/src/vscode-dts/vscode.proposed.extensionLog.d.ts index 9122f0963a3..e575defff20 100644 --- a/src/vscode-dts/vscode.proposed.extensionLog.d.ts +++ b/src/vscode-dts/vscode.proposed.extensionLog.d.ts @@ -5,120 +5,50 @@ declare module 'vscode' { - export interface AbstractOutputChannel { - /** - * The human-readable name of this output channel. - */ - readonly name: string; - - /** - * Append the given value and a line feed character - * to the channel. - * - * @param value A string, falsy values will be printed. - */ - appendLine(value: string): void; - - /** - * Reveal this channel in the UI. - * - * @param preserveFocus When `true` the channel will not take focus. - */ - show(preserveFocus?: boolean): void; - - /** - * Hide this channel from the UI. - */ - hide(): void; - - /** - * Dispose and free associated resources. - */ - dispose(): void; - } - - /** - * An output channel is a container for readonly textual information. - * - * To get an instance of an `OutputChannel` use - * {@link window.createOutputChannel createOutputChannel}. - */ - export interface OutputChannel extends AbstractOutputChannel { - - /** - * Append the given value to the channel. - * - * @param value A string, falsy values will not be printed. - */ - append(value: string): void; - - /** - * Replaces all output from the channel with the given value. - * - * @param value A string, falsy values will not be printed. - */ - replace(value: string): void; - - /** - * Removes all output from the channel. - */ - clear(): void; - - /** - * Reveal this channel in the UI. - * - * @deprecated Use the overload with just one parameter (`show(preserveFocus?: boolean): void`). - * - * @param column This argument is **deprecated** and will be ignored. - * @param preserveFocus When `true` the channel will not take focus. - */ - show(column?: ViewColumn, preserveFocus?: boolean): void; - } - /** * A channel for containing log output. */ - export interface LogOutputChannel extends AbstractOutputChannel { + export interface LogOutputChannel extends OutputChannel { /** * Log the given trace message to the channel. * - * Messages are only printed when the user has enabled trace logging for the extension. + * Messages are only printed when the user has enabled trace logging. * * @param message trace message to log */ - trace(message: string): void; + trace(message: string, ...args: any[]): void; /** * Log the given debug message to the channel. * - * Messages are only printed when the user has enabled debug logging for the extension. + * Messages are only printed when the user has enabled debug logging. * * @param message debug message to log */ - debug(message: string): void; + debug(message: string, ...args: any[]): void; /** * Log the given info message to the channel. * - * Messages are only printed when the user has enabled info logging for the extension. + * Messages are only printed when the user has enabled info logging. * * @param message info message to log */ - info(message: string): void; + info(message: string, ...args: any[]): void; /** * Log the given warning message to the channel. * - * Messages are only printed when the user has enabled warn logging for the extension. + * Messages are only printed when the user has enabled warn logging. * * @param message warning message to log */ - warn(message: string): void; + warn(message: string, ...args: any[]): void; /** * Log the given error or error message to the channel. * - * Messages are only printed when the user has enabled error logging for the extension. + * Messages are only printed when the user has enabled error logging. * * @param error Error or error message to log */ - error(error: string | Error): void; + error(error: string | Error, ...args: any[]): void; } export namespace window { diff --git a/src/vscode-dts/vscode.proposed.localization.d.ts b/src/vscode-dts/vscode.proposed.localization.d.ts index 538dc68d108..e57d4477b74 100644 --- a/src/vscode-dts/vscode.proposed.localization.d.ts +++ b/src/vscode-dts/vscode.proposed.localization.d.ts @@ -8,11 +8,11 @@ declare module 'vscode' { /** * A string that can be pulled out of a localization bundle if it exists. */ - export function t(message: string, ...args: string[]): string; + export function t(message: string, ...args: any[]): string; /** * A string that can be pulled out of a localization bundle if it exists. */ - export function t(options: { message: string; args: string[]; comment: string[] }): string; + export function t(options: { message: string; args?: any[]; comment: string[] }): string; /** * The bundle of localized strings that have been loaded for the extension. */ diff --git a/src/vscode-dts/vscode.proposed.notebookControllerAffinityHidden.d.ts b/src/vscode-dts/vscode.proposed.notebookControllerAffinityHidden.d.ts new file mode 100644 index 00000000000..d35edb91f53 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.notebookControllerAffinityHidden.d.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + // https://github.com/microsoft/vscode/issues/161144 + export enum NotebookControllerAffinity2 { + Default = 1, + Preferred = 2, + Hidden = -1 + } + + export interface NotebookController { + updateNotebookAffinity(notebook: NotebookDocument, affinity: NotebookControllerAffinity | NotebookControllerAffinity2): void; + } +} diff --git a/yarn.lock b/yarn.lock index cff00046895..99c9297fccf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -637,13 +637,13 @@ node-addon-api "^3.2.1" node-gyp-build "^4.3.0" -"@playwright/test@1.24.2": - version "1.24.2" - resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.24.2.tgz#283ea8cc497f9742037458659bf235f4776cf1f0" - integrity sha512-Q4X224pRHw4Dtkk5PoNJplZCokLNvVbXD9wDQEMrHcEuvWpJWEQDeJ9gEwkZ3iCWSFSWBshIX177B231XW4wOQ== +"@playwright/test@1.26.0": + version "1.26.0" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.26.0.tgz#d0c4a7ffaa7df5b4a63e0d8dea133fdb1d6c5aef" + integrity sha512-D24pu1k/gQw3Lhbpc38G5bXlBjGDrH5A52MsrH12wz6ohGDeQ+aZg/JFSEsT/B3G8zlJe/EU4EkJK74hpqsjEg== dependencies: "@types/node" "*" - playwright-core "1.24.2" + playwright-core "1.26.0" "@sindresorhus/is@^0.14.0": version "0.14.0" @@ -8464,10 +8464,10 @@ playwright-core@1.23.4: resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.23.4.tgz#e8a45e549faf6bfad24a0e9998e451979514d41e" integrity sha512-h5V2yw7d8xIwotjyNrkLF13nV9RiiZLHdXeHo+nVJIYGVlZ8U2qV0pMxNJKNTvfQVT0N8/A4CW6/4EW2cOcTiA== -playwright-core@1.24.2: - version "1.24.2" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.24.2.tgz#47bc5adf3dcfcc297a5a7a332449c9009987db26" - integrity sha512-zfAoDoPY/0sDLsgSgLZwWmSCevIg1ym7CppBwllguVBNiHeixZkc1AdMuYUPZC6AdEYc4CxWEyLMBTw2YcmRrA== +playwright-core@1.26.0: + version "1.26.0" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.26.0.tgz#850228f0638d410a5cdd69800d552f60e4d295cd" + integrity sha512-p8huU8eU4gD3VkJd3DA1nA7R3XA6rFvFL+1RYS96cSljCF2yJE9CWEHTPF4LqX8KN9MoWCrAfVKP5381X3CZqg== playwright@^1.23.1: version "1.23.4" @@ -10817,10 +10817,10 @@ typescript@^2.6.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4" integrity sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q= -typescript@^4.9.0-dev.20220916: - version "4.9.0-dev.20220916" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.0-dev.20220916.tgz#9cf0a7e64385f9ee9c841bbd1ba9b0bfd113301b" - integrity sha512-UuHLjqwsPLARYlNFwXEblQsiPFPRbZEx8dbitbfbx5l++DCAL2/HZdZ3VMWAS84KRFCwUxpYXdD0EvBU5S79KQ== +typescript@^4.9.0-dev.20220921: + version "4.9.0-dev.20220921" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.0-dev.20220921.tgz#ad0a24dcc94a29ee8d3845ad025ced94fe732d82" + integrity sha512-0WdiNgM0YfgPURswuah1JJfnDwa6e7Ki0DJcfAEruw17mACMCJw2F7vK/4jLcd1uIl4La3ZIPnA7dOyM7y5Skg== typical@^4.0.0: version "4.0.0" @@ -11628,40 +11628,40 @@ xtend@~2.1.1: dependencies: object-keys "~0.4.0" -xterm-addon-canvas@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/xterm-addon-canvas/-/xterm-addon-canvas-0.2.0.tgz#ba0080d4071f172f94e8c0b5e6151dd7e386f1a1" - integrity sha512-b4tMT05US9Rlqv6R0XZTHsfq8MRKzwxITwpvckuod/l4lokcCokHPbgpYAytOgrzqkzWjYI+Ol8en6cMGf8ncg== +xterm-addon-canvas@0.3.0-beta.1: + version "0.3.0-beta.1" + resolved "https://registry.yarnpkg.com/xterm-addon-canvas/-/xterm-addon-canvas-0.3.0-beta.1.tgz#17a65f5da65416b01d620ddef6247ff5013ffc15" + integrity sha512-34PKhrkvK1RtlOOmni4i5GUIyoFKGMph8fWFvA2d52IDTKmX9YoLzZfU73D/sUAx+/GKobCE8sr14CuBZctgNw== -xterm-addon-search@0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.10.0.tgz#b6a5e859c0bfd83ad534233f93376640c0e0c652" - integrity sha512-l+kjDxNDQbkniU5OUo9BHknxUEPZGM0OFpVpc2sMmrb97S0FKJVJO4wAZPJvSGVJ8ZEG6KuDyzXluvnb08t71Q== +xterm-addon-search@0.11.0-beta.1: + version "0.11.0-beta.1" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.11.0-beta.1.tgz#fe7178d70246cde73550447c5524672575467499" + integrity sha512-fKj8KnnhH1nC4oZpKsgnhtgxkTctoa9kGLMpTJjsNzFu0VvXvLGIRezTPI75UEIQdEdaxcwB7/aKelQTO+72LA== -xterm-addon-serialize@0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.8.0.tgz#715b510b91cc8e0d32844ca2a7de4d0fb5d49d9d" - integrity sha512-8N4RBaxM4TwlZRFXYz+xJwHUeFRXscRIM9O3wq26T0DrG7lM9JprSq1F4IGO1I5Et/CqXjiBFD50KwqkRKF0/w== +xterm-addon-serialize@0.9.0-beta.1: + version "0.9.0-beta.1" + resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.9.0-beta.1.tgz#44a8047ec85abe4db232acc58c53355dd314bf6d" + integrity sha512-jVkpU5GC728ko0k190o+M1xubMkhRolKj18160rxlZhd0Sm/1yHUtFneC9pYSsLypynd3Te5LnZnHfEgVmka4g== -xterm-addon-unicode11@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.4.0.tgz#59a4abbb591befb69ca0c5f7c3f9fa9c1c05353e" - integrity sha512-HkUwR4gc8MKVFy2Ux8zUnjqARYpfl7dJ9na3TwRbAUbF4JlCv707m4Z07WVaDMIRUZsfZ+5LgSi+Ss7PfZqNcw== +xterm-addon-unicode11@0.5.0-beta.1: + version "0.5.0-beta.1" + resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.5.0-beta.1.tgz#8a9e9356018e082318abbe2be1f9599fcc6b46a2" + integrity sha512-uAErX4gwhW6N524stLG6oZR3yBGgPnFmZ2Tv4vyYy7tcgDuHRoc22xYSCDgO1ohz1FLlOm8JGXRjXliwO9ic3A== -xterm-addon-webgl@0.13.0: - version "0.13.0" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.13.0.tgz#b1d42ec454390ad8595aa8c8dde714b98a5eb896" - integrity sha512-xL4qBQWUHjFR620/8VHCtrTMVQsnZaAtd1IxFoiKPhC63wKp6b+73a45s97lb34yeo57PoqZhE9Jq5pB++ksPQ== +xterm-addon-webgl@0.14.0-beta.2: + version "0.14.0-beta.2" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.14.0-beta.2.tgz#832c31b52b78fb67a65bbd23c9fb850caceb43ae" + integrity sha512-1ccbkJiUZ5ojnoAEJsbdV0jMZaYSnZ02wfV8yBU243u6TTgvCzZ7nq5BR9bT+5K/ESFWiekobfybxHwuYnylmQ== -xterm-headless@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.0.0.tgz#6426e85aec24f1bdfd322fe4e5159e0113f059d4" - integrity sha512-d+3C883Tz5zNCcapJSUpZVO8lplKhjJZtvuL4oIMTE74BX/3UsK7zUQgp5c6KS5Vv4FjFSb3XQA+n2Cx9Znq+w== +xterm-headless@5.1.0-beta.1: + version "5.1.0-beta.1" + resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.1.0-beta.1.tgz#badec2e97e47aa44267a4de2c1b42b4d23ad49a2" + integrity sha512-V3G7l4pN6/HW//vKXryOCdDXVKdrQTQmtHEqkZ8waD68cJdeMdIoGYJuzavd5rHpxCqm/KR5O8ztI41jridong== -xterm@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.0.0.tgz#0af50509b33d0dc62fde7a4ec17750b8e453cc5c" - integrity sha512-tmVsKzZovAYNDIaUinfz+VDclraQpPUnAME+JawosgWRMphInDded/PuY0xmU5dOhyeYZsI0nz5yd8dPYsdLTA== +xterm@5.1.0-beta.1: + version "5.1.0-beta.1" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.1.0-beta.1.tgz#a6617c6887066d166632d1e69b6eb83a179d8b63" + integrity sha512-ml7bqjO23bh4yu7qXKogXtCy4SbDTV21rfDXUvLPPaxrlQus6NoN1byy1eFH4ONWpv5ZHGeItRdQ/X00et9Pcw== y18n@^3.2.1: version "3.2.2"