diff --git a/.eslintrc.json b/.eslintrc.json index e1dc514e65e..5414b07f942 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -669,6 +669,7 @@ "create", "delete", "dispose", + "edit", "end", "expand", "hide", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 18d4d467786..59cde5076c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -116,3 +116,29 @@ jobs: name: Run Unit Tests (Browser) - run: ./scripts/test-integration.sh --tfs "Integration Tests" name: Run Integration Tests (Electron) + + monaco: + runs-on: ubuntu-latest + env: + CHILD_CONCURRENCY: "1" + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v1 + # TODO: rename azure-pipelines/linux/xvfb.init to github-actions + - run: | + sudo apt-get update + sudo apt-get install -y libxkbfile-dev pkg-config libsecret-1-dev libxss1 dbus xvfb libgtk-3-0 libgbm1 + sudo cp build/azure-pipelines/linux/xvfb.init /etc/init.d/xvfb + sudo chmod +x /etc/init.d/xvfb + sudo update-rc.d xvfb defaults + sudo service xvfb start + name: Setup Build Environment + - uses: actions/setup-node@v1 + with: + node-version: 10 + - run: yarn --frozen-lockfile + name: Install Dependencies + - run: yarn monaco-compile-check + name: Run Monaco Editor Checks + - run: yarn gulp editor-esm-bundle + name: Editor Distro & ESM Bundle diff --git a/.gitignore b/.gitignore index 160c42ed74b..e73dd4d9e8c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ out-editor/ out-editor-src/ out-editor-build/ out-editor-esm/ +out-editor-esm-bundle/ out-editor-min/ out-monaco-editor-core/ out-vscode/ diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 7bd723dcbff..7eaccc2a893 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -23,17 +23,17 @@ This project incorporates components from the projects listed below. The origina 16. fadeevab/make.tmbundle (https://github.com/fadeevab/make.tmbundle) 17. freebroccolo/atom-language-swift (https://github.com/freebroccolo/atom-language-swift) 18. HTML 5.1 W3C Working Draft version 08 October 2015 (http://www.w3.org/TR/2015/WD-html51-20151008/) -19. Ikuyadeu/vscode-R version 0.5.5 (https://github.com/Ikuyadeu/vscode-R) +19. Ikuyadeu/vscode-R version 1.1.8 (https://github.com/Ikuyadeu/vscode-R) 20. insane version 2.6.2 (https://github.com/bevacqua/insane) 21. Ionic documentation version 1.2.4 (https://github.com/ionic-team/ionic-site) 22. ionide/ionide-fsgrammar (https://github.com/ionide/ionide-fsgrammar) 23. jeff-hykin/cpp-textmate-grammar version 1.12.11 (https://github.com/jeff-hykin/cpp-textmate-grammar) -24. jeff-hykin/cpp-textmate-grammar version 1.14.15 (https://github.com/jeff-hykin/cpp-textmate-grammar) +24. jeff-hykin/cpp-textmate-grammar version 1.14.20 (https://github.com/jeff-hykin/cpp-textmate-grammar) 25. js-beautify version 1.6.8 (https://github.com/beautify-web/js-beautify) 26. Jxck/assert version 1.0.0 (https://github.com/Jxck/assert) 27. language-docker (https://github.com/moby/moby) 28. language-less version 0.34.2 (https://github.com/atom/language-less) -29. language-php version 0.44.3 (https://github.com/atom/language-php) +29. language-php version 0.44.4 (https://github.com/atom/language-php) 30. language-rust version 0.4.12 (https://github.com/zargony/atom-language-rust) 31. MagicStack/MagicPython version 1.1.1 (https://github.com/MagicStack/MagicPython) 32. marked version 0.6.2 (https://github.com/markedjs/marked) @@ -64,7 +64,7 @@ This project incorporates components from the projects listed below. The origina 57. TypeScript-TmLanguage version 1.0.0 (https://github.com/Microsoft/TypeScript-TmLanguage) 58. Unicode version 12.0.0 (http://www.unicode.org/) 59. vscode-codicons version 0.0.1 (https://github.com/microsoft/vscode-codicons) -60. vscode-logfile-highlighter version 2.5.0 (https://github.com/emilast/vscode-logfile-highlighter) +60. vscode-logfile-highlighter version 2.6.0 (https://github.com/emilast/vscode-logfile-highlighter) 61. vscode-swift version 0.0.1 (https://github.com/owensd/vscode-swift) 62. Web Background Synchronization (https://github.com/WICG/BackgroundSync) @@ -2807,4 +2807,4 @@ Apache License See the License for the specific language governing permissions and limitations under the License. ========================================= -END OF Web Background Synchronization NOTICES AND INFORMATION +END OF Web Background Synchronization NOTICES AND INFORMATION \ No newline at end of file diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index e527b8b134f..48916189326 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -77,6 +77,21 @@ steps: yarn postinstall displayName: Run postinstall scripts condition: and(succeeded(), ne(variables['CacheExists-Compilation'], 'true'), eq(variables['CacheRestored'], 'true')) + env: + OSS_GITHUB_ID: "a5d3c261b032765a78de" + OSS_GITHUB_SECRET: $(oss-github-client-secret) + INSIDERS_GITHUB_ID: "31f02627809389d9f111" + INSIDERS_GITHUB_SECRET: $(insiders-github-client-secret) + STABLE_GITHUB_ID: "baa8a44b5e861d918709" + STABLE_GITHUB_SECRET: $(stable-github-client-secret) + EXPLORATION_GITHUB_ID: "94e8376d3a90429aeaea" + EXPLORATION_GITHUB_SECRET: $(exploration-github-client-secret) + VSO_GITHUB_ID: "3d4be8f37a0325b5817d" + VSO_GITHUB_SECRET: $(vso-github-client-secret) + VSO_PPE_GITHUB_ID: "eabf35024dc2e891a492" + VSO_PPE_GITHUB_SECRET: $(vso-ppe-github-client-secret) + VSO_DEV_GITHUB_ID: "84383ebd8a7c5f5efc5c" + VSO_DEV_GITHUB_SECRET: $(vso-dev-github-client-secret) # Mixin must run before optimize, because the CSS loader will # inline small SVGs @@ -116,11 +131,6 @@ steps: yarn gulp minify-vscode-reh-web displayName: Compile condition: and(succeeded(), ne(variables['CacheExists-Compilation'], 'true')) - env: - OSS_GITHUB_ID: "a5d3c261b032765a78de" - OSS_GITHUB_SECRET: $(oss-github-client-secret) - INSIDERS_GITHUB_ID: "31f02627809389d9f111" - INSIDERS_GITHUB_SECRET: $(insiders-github-client-secret) - script: | set -e diff --git a/build/builtInExtensions.json b/build/builtInExtensions.json index 9bd52b930f6..ddc65ea119f 100644 --- a/build/builtInExtensions.json +++ b/build/builtInExtensions.json @@ -1,7 +1,7 @@ [ { "name": "ms-vscode.node-debug", - "version": "1.43.0", + "version": "1.43.2", "repo": "https://github.com/Microsoft/vscode-node-debug", "metadata": { "id": "b6ded8fb-a0a0-4c1c-acbd-ab2a3bc995a6", @@ -46,7 +46,7 @@ }, { "name": "ms-vscode.js-debug-nightly", - "version": "2020.2.1417", + "version": "2020.2.2517", "forQualities": [ "insider" ], diff --git a/build/gulpfile.editor.js b/build/gulpfile.editor.js index 105402470d8..9278f689ec3 100644 --- a/build/gulpfile.editor.js +++ b/build/gulpfile.editor.js @@ -16,6 +16,8 @@ const cp = require('child_process'); const compilation = require('./lib/compilation'); const monacoapi = require('./monaco/api'); const fs = require('fs'); +const webpack = require('webpack'); +const webpackGulp = require('webpack-stream'); let root = path.dirname(__dirname); let sha1 = util.getVersion(root); @@ -358,6 +360,49 @@ gulp.task('editor-distro', ) ); +const bundleEditorESMTask = task.define('editor-esm-bundle-webpack', () => { + const result = es.through(); + + const webpackConfigPath = path.join(root, 'build/monaco/monaco.webpack.config.js'); + + const webpackConfig = { + ...require(webpackConfigPath), + ...{ mode: 'production' } + }; + + const webpackDone = (err, stats) => { + if (err) { + result.emit('error', err); + return; + } + const { compilation } = stats; + if (compilation.errors.length > 0) { + result.emit('error', compilation.errors.join('\n')); + } + if (compilation.warnings.length > 0) { + result.emit('data', compilation.warnings.join('\n')); + } + }; + + return webpackGulp(webpackConfig, webpack, webpackDone) + .pipe(gulp.dest('out-editor-esm-bundle')); +}); + +gulp.task('editor-esm-bundle', + task.series( + task.parallel( + util.rimraf('out-editor-src'), + util.rimraf('out-editor-esm'), + util.rimraf('out-monaco-editor-core'), + util.rimraf('out-editor-esm-bundle'), + ), + extractEditorSrcTask, + createESMSourcesAndResourcesTask, + compileEditorESMTask, + bundleEditorESMTask, + ) +); + gulp.task('monacodts', task.define('monacodts', () => { const result = monacoapi.execute(); fs.writeFileSync(result.filePath, result.content); diff --git a/build/lib/compilation.js b/build/lib/compilation.js index 96ef00c0657..59bf1a250f6 100644 --- a/build/lib/compilation.js +++ b/build/lib/compilation.js @@ -74,7 +74,6 @@ function compileTask(src, out, build) { if (src === 'src') { generator.execute(); } - generateGitHubAuthConfig(); return srcPipe .pipe(generator.stream) .pipe(compile()) @@ -97,17 +96,6 @@ function watchTask(out, build) { } exports.watchTask = watchTask; const REPO_SRC_FOLDER = path.join(__dirname, '../../src'); -function generateGitHubAuthConfig() { - const schemes = ['OSS', 'INSIDERS']; - let content = {}; - schemes.forEach(scheme => { - content[scheme] = { - id: process.env[`${scheme}_GITHUB_ID`], - secret: process.env[`${scheme}_GITHUB_SECRET`] - }; - }); - fs.writeFileSync(path.join(__dirname, '../../extensions/github-authentication/src/common/config.json'), JSON.stringify(content)); -} class MonacoGenerator { constructor(isWatch) { this._executeSoonTimer = null; diff --git a/build/lib/compilation.ts b/build/lib/compilation.ts index 0d225d89972..578fae31a19 100644 --- a/build/lib/compilation.ts +++ b/build/lib/compilation.ts @@ -88,8 +88,6 @@ export function compileTask(src: string, out: string, build: boolean): () => Nod generator.execute(); } - generateGitHubAuthConfig(); - return srcPipe .pipe(generator.stream) .pipe(compile()) @@ -117,19 +115,6 @@ export function watchTask(out: string, build: boolean): () => NodeJS.ReadWriteSt const REPO_SRC_FOLDER = path.join(__dirname, '../../src'); -function generateGitHubAuthConfig() { - const schemes = ['OSS', 'INSIDERS']; - let content: { [key: string]: { id?: string, secret?: string }} = {}; - schemes.forEach(scheme => { - content[scheme] = { - id: process.env[`${scheme}_GITHUB_ID`], - secret: process.env[`${scheme}_GITHUB_SECRET`] - }; - }); - - fs.writeFileSync(path.join(__dirname, '../../extensions/github-authentication/src/common/config.json'), JSON.stringify(content)); -} - class MonacoGenerator { private readonly _isWatch: boolean; public readonly stream: NodeJS.ReadWriteStream; diff --git a/build/monaco/api.js b/build/monaco/api.js index 0fbaf7335b7..bd656f2bcd1 100644 --- a/build/monaco/api.js +++ b/build/monaco/api.js @@ -135,11 +135,12 @@ function getMassagedTopLevelDeclarationText(sourceFile, declaration, importName, } else { const memberName = member.name.text; + const memberAccess = (memberName.indexOf('.') >= 0 ? `['${memberName}']` : `.${memberName}`); if (isStatic(member)) { - usage.push(`a = ${staticTypeName}.${memberName};`); + usage.push(`a = ${staticTypeName}${memberAccess};`); } else { - usage.push(`a = (<${instanceTypeName}>b).${memberName};`); + usage.push(`a = (<${instanceTypeName}>b)${memberAccess};`); } } } diff --git a/build/monaco/api.ts b/build/monaco/api.ts index 511768ee64b..f3542988a4b 100644 --- a/build/monaco/api.ts +++ b/build/monaco/api.ts @@ -167,10 +167,11 @@ function getMassagedTopLevelDeclarationText(sourceFile: ts.SourceFile, declarati result = result.replace(memberText, ''); } else { const memberName = (member.name).text; + const memberAccess = (memberName.indexOf('.') >= 0 ? `['${memberName}']` : `.${memberName}`); if (isStatic(member)) { - usage.push(`a = ${staticTypeName}.${memberName};`); + usage.push(`a = ${staticTypeName}${memberAccess};`); } else { - usage.push(`a = (<${instanceTypeName}>b).${memberName};`); + usage.push(`a = (<${instanceTypeName}>b)${memberAccess};`); } } } catch (err) { diff --git a/build/monaco/esm.core.js b/build/monaco/esm.core.js new file mode 100644 index 00000000000..b84b5fb4f41 --- /dev/null +++ b/build/monaco/esm.core.js @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// Entry file for webpack bunlding. + +import * as monaco from 'monaco-editor-core'; + +self.MonacoEnvironment = { + getWorkerUrl: function (moduleId, label) { + return './editor.worker.bundle.js'; + } +}; + +monaco.editor.create(document.getElementById('container'), { + value: [ + 'var hello = "hello world";' + ].join('\n'), + language: 'javascript' +}); diff --git a/build/monaco/monaco.webpack.config.js b/build/monaco/monaco.webpack.config.js new file mode 100644 index 00000000000..974a341a197 --- /dev/null +++ b/build/monaco/monaco.webpack.config.js @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +const path = require('path'); + +module.exports = { + mode: 'production', + entry: { + 'core': './build/monaco/esm.core.js', + 'editor.worker': './out-monaco-editor-core/esm/vs/editor/editor.worker.js' + }, + output: { + globalObject: 'self', + filename: '[name].bundle.js', + path: path.resolve(__dirname, 'dist') + }, + module: { + rules: [{ + test: /\.css$/, + use: ['style-loader', 'css-loader'] + }, { + test: /\.ttf$/, + use: ['file-loader'] + }] + }, + resolve: { + alias: { + 'monaco-editor-core': path.resolve(__dirname, '../../out-monaco-editor-core/esm/vs/editor/editor.main.js'), + } + }, + stats: { + all: false, + modules: true, + maxModules: 0, + errors: true, + warnings: true, + // our additional options + moduleTrace: true, + errorDetails: true, + chunks: true + } +}; diff --git a/build/npm/postinstall.js b/build/npm/postinstall.js index 5a937a80bd8..7a2320d8289 100644 --- a/build/npm/postinstall.js +++ b/build/npm/postinstall.js @@ -13,7 +13,7 @@ const yarn = process.platform === 'win32' ? 'yarn.cmd' : 'yarn'; * @param {*} [opts] */ function yarnInstall(location, opts) { - opts = opts || {}; + opts = opts || { env: process.env }; opts.cwd = location; opts.stdio = 'inherit'; @@ -52,8 +52,6 @@ extensions.forEach(extension => yarnInstall(`extensions/${extension}`)); function yarnInstallBuildDependencies() { // make sure we install the deps of build/lib/watch for the system installed // node, since that is the driver of gulp - //@ts-ignore - const env = Object.assign({}, process.env); const watchPath = path.join(path.dirname(__dirname), 'lib', 'watch'); const yarnrcPath = path.join(watchPath, '.yarnrc'); @@ -66,7 +64,7 @@ target "${target}" runtime "${runtime}"`; fs.writeFileSync(yarnrcPath, yarnrc, 'utf8'); - yarnInstall(watchPath, { env }); + yarnInstall(watchPath); } yarnInstall(`build`); // node modules required for build diff --git a/cglicenses.json b/cglicenses.json index 1e9287cbaf2..76e6fe49f00 100644 --- a/cglicenses.json +++ b/cglicenses.json @@ -81,5 +81,13 @@ "prependLicenseText": [ "Copyright (c) Microsoft Corporation. All rights reserved." ] + }, + { + // Reason: The license at https://github.com/rbuckton/reflect-metadata/blob/master/LICENSE + // does not include a clear Copyright statement (it's in https://github.com/rbuckton/reflect-metadata/blob/master/CopyrightNotice.txt). + "name": "reflect-metadata", + "prependLicenseText": [ + "Copyright (c) Microsoft Corporation. All rights reserved." + ] } ] diff --git a/extensions/configuration-editing/package.json b/extensions/configuration-editing/package.json index be2d2e8ebae..0defd3aee6f 100644 --- a/extensions/configuration-editing/package.json +++ b/extensions/configuration-editing/package.json @@ -18,8 +18,8 @@ "watch": "gulp watch-extension:configuration-editing" }, "dependencies": { - "jsonc-parser": "^2.1.1", - "vscode-nls": "^4.0.0" + "jsonc-parser": "^2.2.1", + "vscode-nls": "^4.1.1" }, "contributes": { "languages": [ diff --git a/extensions/configuration-editing/schemas/devContainer.schema.json b/extensions/configuration-editing/schemas/devContainer.schema.json index 0d47b27bec9..9cb171915fc 100644 --- a/extensions/configuration-editing/schemas/devContainer.schema.json +++ b/extensions/configuration-editing/schemas/devContainer.schema.json @@ -48,6 +48,16 @@ "type": "string", "description": "The user VS Code Server will be started with. The default is the same user as the container." }, + "initializeCommand": { + "type": [ + "string", + "array" + ], + "description": "A command to run locally before anything else. If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.", + "items": { + "type": "string" + } + }, "postCreateCommand": { "type": [ "string", @@ -132,21 +142,89 @@ } } }, - "dockerFileContainer": { + "dockerfileContainer": { + "oneOf": [ + { + "type": "object", + "properties": { + "build": { + "type": "object", + "description": "Docker build-related options.", + "allOf": [ + { + "type": "object", + "properties": { + "dockerfile": { + "type": "string", + "description": "The location of the Dockerfile that defines the contents of the container. The path is relative to the folder containing the `devcontainer.json` file." + }, + "context": { + "type": "string", + "description": "The location of the context folder for building the Docker image. The path is relative to the folder containing the `devcontainer.json` file." + } + }, + "required": [ + "dockerfile" + ] + }, + { + "$ref": "#/definitions/buildOptions" + } + ] + } + }, + "required": [ + "build" + ] + }, + { + "allOf": [ + { + "type": "object", + "properties": { + "dockerFile": { + "type": "string", + "description": "The location of the Dockerfile that defines the contents of the container. The path is relative to the folder containing the `devcontainer.json` file." + }, + "context": { + "type": "string", + "description": "The location of the context folder for building the Docker image. The path is relative to the folder containing the `devcontainer.json` file." + } + }, + "required": [ + "dockerFile" + ] + }, + { + "type": "object", + "properties": { + "build": { + "description": "Docker build-related options.", + "$ref": "#/definitions/buildOptions" + } + } + } + ] + } + ] + }, + "buildOptions": { "type": "object", "properties": { - "dockerFile": { + "target": { "type": "string", - "description": "The location of the Dockerfile that defines the contents of the container. The path is relative to the folder containing the `devcontainer.json` file." + "description": "Target stage in a multi-stage build." }, - "context": { - "type": "string", - "description": "The location of the context folder for building the Docker image. The path is relative to the folder containing the `devcontainer.json` file." + "args": { + "type": "object", + "additionalProperties": { + "type": [ + "string" + ] + }, + "description": "Build arguments." } - }, - "required": [ - "dockerFile" - ] + } }, "imageContainer": { "type": "object", @@ -212,7 +290,7 @@ { "oneOf": [ { - "$ref": "#/definitions/dockerFileContainer" + "$ref": "#/definitions/dockerfileContainer" }, { "$ref": "#/definitions/imageContainer" diff --git a/extensions/configuration-editing/yarn.lock b/extensions/configuration-editing/yarn.lock index d5aafed1189..36aab5fd224 100644 --- a/extensions/configuration-editing/yarn.lock +++ b/extensions/configuration-editing/yarn.lock @@ -7,12 +7,12 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.11.7.tgz#57682a9771a3f7b09c2497f28129a0462966524a" integrity sha512-JNbGaHFCLwgHn/iCckiGSOZ1XYHsKFwREtzPwSGCVld1SGhOlmZw2D4ZI94HQCrBHbADzW9m4LER/8olJTRGHA== -jsonc-parser@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.1.1.tgz#83dc3d7a6e7186346b889b1280eefa04446c6d3e" - integrity sha512-VC0CjnWJylKB1iov4u76/W/5Ef0ydDkjtYWxoZ9t3HdWlSnZQwZL5MgFikaB/EtQ4RmMEw3tmQzuYnZA2/Ja1g== +jsonc-parser@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.2.1.tgz#db73cd59d78cce28723199466b2a03d1be1df2bc" + integrity sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w== -vscode-nls@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.0.0.tgz#4001c8a6caba5cedb23a9c5ce1090395c0e44002" - integrity sha512-qCfdzcH+0LgQnBpZA53bA32kzp9rpq/f66Som577ObeuDlFIrtbEJ+A/+CCxjIh4G8dpJYNCKIsxpRAHIfsbNw== +vscode-nls@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.1.tgz#f9916b64e4947b20322defb1e676a495861f133c" + integrity sha512-4R+2UoUUU/LdnMnFjePxfLqNhBS8lrAFyX7pjb2ud/lqDkrUavFUTcG7wR0HBZFakae0Q6KLBFjMS6W93F403A== diff --git a/extensions/css-language-features/package.json b/extensions/css-language-features/package.json index d10cbc6e10c..4d3cffac258 100644 --- a/extensions/css-language-features/package.json +++ b/extensions/css-language-features/package.json @@ -806,7 +806,7 @@ ] }, "dependencies": { - "vscode-languageclient": "^6.1.0", + "vscode-languageclient": "^6.1.1", "vscode-nls": "^4.1.1" }, "devDependencies": { diff --git a/extensions/css-language-features/server/package.json b/extensions/css-language-features/server/package.json index b13b8f650e1..7f439dd686a 100644 --- a/extensions/css-language-features/server/package.json +++ b/extensions/css-language-features/server/package.json @@ -9,11 +9,11 @@ }, "main": "./out/cssServerMain", "dependencies": { - "vscode-css-languageservice": "^4.0.3-next.27", - "vscode-languageserver": "^6.1.0" + "vscode-css-languageservice": "^4.1.0", + "vscode-languageserver": "^6.1.1" }, "devDependencies": { - "@types/mocha": "2.2.33", + "@types/mocha": "7.0.1", "@types/node": "^12.11.7", "glob": "^7.1.6", "mocha": "^7.0.1", diff --git a/extensions/css-language-features/server/yarn.lock b/extensions/css-language-features/server/yarn.lock index cb93fa97899..e57ff40f62e 100644 --- a/extensions/css-language-features/server/yarn.lock +++ b/extensions/css-language-features/server/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/mocha@2.2.33": - version "2.2.33" - resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-2.2.33.tgz#d79a0061ec270379f4d9e225f4096fb436669def" - integrity sha1-15oAYewnA3n02eIl9AlvtDZmne8= +"@types/mocha@7.0.1": + version "7.0.1" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-7.0.1.tgz#5d7ec2a789a1f77c59b7ad071b9d50bf1abbfc9e" + integrity sha512-L/Nw/2e5KUaprNJoRA33oly+M8X8n0K+FwLTbYqwTcR14wdPWeRkigBLfSFpN/Asf9ENZTMZwLxjtjeYucAA4Q== "@types/node@^12.11.7": version "12.11.7" @@ -689,12 +689,12 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -vscode-css-languageservice@^4.0.3-next.27: - version "4.0.3-next.27" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.0.3-next.27.tgz#04f52bfdd85ac7eed8d82a0133aaabb4c0e18f7f" - integrity sha512-MU8sHQABb1WnzOwgazIdO4lXG7cGzFKd7VKi5j67uWTNsqSrbAVSoKGjSyOLq/o6h1L5DGG1Og/7q403z6D04g== +vscode-css-languageservice@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.1.0.tgz#144c8274e0bf1719fa6f773ca684bd1c7ffd634f" + integrity sha512-iTX3dTp0Y0RFWhIux5jasI8r9swdiWVB1Z3OrZ10iDHxzkETjVPxAQ5BEQU4ag0Awc8TTg1C7sJriHQY2LO14g== dependencies: - vscode-languageserver-textdocument "^1.0.1-next.1" + vscode-languageserver-textdocument "^1.0.1" vscode-languageserver-types "^3.15.1" vscode-nls "^4.1.1" vscode-uri "^2.1.1" @@ -704,30 +704,30 @@ vscode-jsonrpc@^5.0.1: resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz#9bab9c330d89f43fc8c1e8702b5c36e058a01794" integrity sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A== -vscode-languageserver-protocol@^3.15.2: - version "3.15.2" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.2.tgz#e52c62923140b2655ad2472f6f29cfb83bacf5b8" - integrity sha512-GdL05JKOgZ76RDg3suiGCl9enESM7iQgGw4x93ibTh4sldvZmakHmTeZ4iUApPPGKf6O3OVBtrsksBXnHYaxNg== +vscode-languageserver-protocol@^3.15.3: + version "3.15.3" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.3.tgz#3fa9a0702d742cf7883cb6182a6212fcd0a1d8bb" + integrity sha512-zrMuwHOAQRhjDSnflWdJG+O2ztMWss8GqUUB8dXLR/FPenwkiBNkMIJJYfSN6sgskvsF0rHAoBowNQfbyZnnvw== dependencies: vscode-jsonrpc "^5.0.1" vscode-languageserver-types "3.15.1" -vscode-languageserver-textdocument@^1.0.1-next.1: - version "1.0.1-next.1" - resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.1-next.1.tgz#c8f2f792c7c88d33ea8441ca04bfb8376896b671" - integrity sha512-Cmt0KsNxouns+d7/Kw/jWtWU9Z3h56z1qAA8utjDOEqrDcrTs2rDXv3EJRa99nuKM3wVf6DbWym1VqL9q71XPA== +vscode-languageserver-textdocument@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.1.tgz#178168e87efad6171b372add1dea34f53e5d330f" + integrity sha512-UIcJDjX7IFkck7cSkNNyzIz5FyvpQfY7sdzVy+wkKN/BLaD4DQ0ppXQrKePomCxTS7RrolK1I0pey0bG9eh8dA== vscode-languageserver-types@3.15.1, vscode-languageserver-types@^3.15.1: version "3.15.1" resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz#17be71d78d2f6236d414f0001ce1ef4d23e6b6de" integrity sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ== -vscode-languageserver@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-6.1.0.tgz#f0ff149b06d1961f49ed03385ecc2a96bcaddcde" - integrity sha512-Q5kUJegYclTZMnKUaEcxJK41Ozp6qJhhoFJYj0w8y8j9JXdKT479LE945QCKRvSgWfsqTSUmgsozVTUIwQQxHw== +vscode-languageserver@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-6.1.1.tgz#d76afc68172c27d4327ee74332b468fbc740d762" + integrity sha512-DueEpkUAkD5XTR4MLYNr6bQIp/UFR0/IPApgXU3YfCBCB08u2sm9hRCs6DxYZELkk++STPjpcjksR2H8qI3cDQ== dependencies: - vscode-languageserver-protocol "^3.15.2" + vscode-languageserver-protocol "^3.15.3" vscode-nls@^4.1.1: version "4.1.1" diff --git a/extensions/css-language-features/yarn.lock b/extensions/css-language-features/yarn.lock index b90ab508aaa..4494f0038a2 100644 --- a/extensions/css-language-features/yarn.lock +++ b/extensions/css-language-features/yarn.lock @@ -640,18 +640,18 @@ vscode-jsonrpc@^5.0.1: resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz#9bab9c330d89f43fc8c1e8702b5c36e058a01794" integrity sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A== -vscode-languageclient@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-6.1.0.tgz#ee67c0b7818c42ce0281572d05c89adfcc4f5a38" - integrity sha512-Tcp0VoOaa0YzxL4nEfK9tsmcy76Eo8jNLvFQZwh2c8oMm02luL8uGYPLQNAiZ3XGgegfcwiQFZMqbW7DNV0vxA== +vscode-languageclient@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-6.1.1.tgz#91b62e416c5abbf2013ae3726f314a19c22a8457" + integrity sha512-mB6d8Tg+82l8EFUfR+SBu0+lCshyKVgC5E5+MQ0/BJa+9AgeBjtG5npoGaCo4/VvWzK0ZRGm85zU5iRp1RYPIA== dependencies: semver "^6.3.0" - vscode-languageserver-protocol "^3.15.2" + vscode-languageserver-protocol "^3.15.3" -vscode-languageserver-protocol@^3.15.2: - version "3.15.2" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.2.tgz#e52c62923140b2655ad2472f6f29cfb83bacf5b8" - integrity sha512-GdL05JKOgZ76RDg3suiGCl9enESM7iQgGw4x93ibTh4sldvZmakHmTeZ4iUApPPGKf6O3OVBtrsksBXnHYaxNg== +vscode-languageserver-protocol@^3.15.3: + version "3.15.3" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.3.tgz#3fa9a0702d742cf7883cb6182a6212fcd0a1d8bb" + integrity sha512-zrMuwHOAQRhjDSnflWdJG+O2ztMWss8GqUUB8dXLR/FPenwkiBNkMIJJYfSN6sgskvsF0rHAoBowNQfbyZnnvw== dependencies: vscode-jsonrpc "^5.0.1" vscode-languageserver-types "3.15.1" diff --git a/extensions/extension-editing/package.json b/extensions/extension-editing/package.json index 54edf5ccbdc..fe4a7b44d2a 100644 --- a/extensions/extension-editing/package.json +++ b/extensions/extension-editing/package.json @@ -19,10 +19,10 @@ "watch": "gulp watch-extension:extension-editing" }, "dependencies": { - "jsonc-parser": "^2.1.1", + "jsonc-parser": "^2.2.1", "markdown-it": "^8.3.1", "parse5": "^3.0.2", - "vscode-nls": "^4.0.0" + "vscode-nls": "^4.1.1" }, "contributes": { "jsonValidation": [ diff --git a/extensions/extension-editing/yarn.lock b/extensions/extension-editing/yarn.lock index d2f69a169d8..50adf31c099 100644 --- a/extensions/extension-editing/yarn.lock +++ b/extensions/extension-editing/yarn.lock @@ -29,10 +29,10 @@ entities@~1.1.1: resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" integrity sha1-blwtClYhtdra7O+AuQ7ftc13cvA= -jsonc-parser@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.1.1.tgz#83dc3d7a6e7186346b889b1280eefa04446c6d3e" - integrity sha512-VC0CjnWJylKB1iov4u76/W/5Ef0ydDkjtYWxoZ9t3HdWlSnZQwZL5MgFikaB/EtQ4RmMEw3tmQzuYnZA2/Ja1g== +jsonc-parser@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.2.1.tgz#db73cd59d78cce28723199466b2a03d1be1df2bc" + integrity sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w== linkify-it@^2.0.0: version "2.0.3" @@ -74,7 +74,7 @@ uc.micro@^1.0.1, uc.micro@^1.0.3: resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.3.tgz#7ed50d5e0f9a9fb0a573379259f2a77458d50192" integrity sha1-ftUNXg+an7ClczeSWfKndFjVAZI= -vscode-nls@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.0.0.tgz#4001c8a6caba5cedb23a9c5ce1090395c0e44002" - integrity sha512-qCfdzcH+0LgQnBpZA53bA32kzp9rpq/f66Som577ObeuDlFIrtbEJ+A/+CCxjIh4G8dpJYNCKIsxpRAHIfsbNw== +vscode-nls@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.1.tgz#f9916b64e4947b20322defb1e676a495861f133c" + integrity sha512-4R+2UoUUU/LdnMnFjePxfLqNhBS8lrAFyX7pjb2ud/lqDkrUavFUTcG7wR0HBZFakae0Q6KLBFjMS6W93F403A== diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 2cd44bc0f05..ad4d47485e7 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -430,25 +430,25 @@ export class CommandCenter { case Status.INDEX_MODIFIED: case Status.INDEX_RENAMED: case Status.INDEX_ADDED: - return `${basename} (Index)`; + return localize('git.title.index', '{0} (Index)', basename); case Status.MODIFIED: case Status.BOTH_ADDED: case Status.BOTH_MODIFIED: - return `${basename} (Working Tree)`; + return localize('git.title.workingTree', '{0} (Working Tree)', basename); case Status.DELETED_BY_US: - return `${basename} (Theirs)`; + return localize('git.title.theirs', '{0} (Theirs)', basename); case Status.DELETED_BY_THEM: - return `${basename} (Ours)`; + return localize('git.title.ours', '{0} (Ours)', basename); case Status.UNTRACKED: + return localize('git.title.untracked', '{0} (Untracked)', basename); - return `${basename} (Untracked)`; + default: + return ''; } - - return ''; } @command('git.clone') @@ -2348,12 +2348,12 @@ export class CommandCenter { let title; if ((item.previousRef === 'HEAD' || item.previousRef === '~') && item.ref === '') { - title = `${basename} (Working Tree)`; + title = localize('git.title.workingTree', '{0} (Working Tree)', basename); } else if (item.previousRef === 'HEAD' && item.ref === '~') { - title = `${basename} (Index)`; + title = localize('git.title.index', '{0} (Index)', basename); } else { - title = `${basename} (${item.shortPreviousRef}) \u27f7 ${basename} (${item.shortRef})`; + title = localize('git.title.diffRefs', '{0} ({1}) \u27f7 {0} ({2})', basename, item.shortPreviousRef, item.shortRef); } return commands.executeCommand('vscode.diff', toGitUri(uri, item.previousRef), item.ref === '' ? uri : toGitUri(uri, item.ref), title); diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 89206132470..ac3120c115a 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -50,8 +50,13 @@ interface MutableRemote extends Remote { * Log file options. */ export interface LogFileOptions { - /** Max number of log entries to retrieve. If not specified, the default is 32. */ - readonly maxEntries?: number; + /** Optional. The maximum number of log entries to retrieve. */ + readonly maxEntries?: number | string; + /** Optional. The Git sha (hash) to start retrieving log entries from. */ + readonly hash?: string; + /** Optional. Specifies whether to start retrieving log entries in reverse order. */ + readonly reverse?: boolean; + readonly sortByAuthorDate?: boolean; } function parseVersion(raw: string): string { @@ -817,8 +822,26 @@ export class Repository { } async logFile(uri: Uri, options?: LogFileOptions): Promise { - const maxEntries = options?.maxEntries ?? 32; - const args = ['log', `-n${maxEntries}`, `--format=${COMMIT_FORMAT}`, '-z', '--', uri.fsPath]; + const args = ['log', `--format=${COMMIT_FORMAT}`, '-z']; + + if (options?.maxEntries && !options?.reverse) { + args.push(`-n${options.maxEntries}`); + } + + if (options?.hash) { + // If we are reversing, we must add a range (with HEAD) because we are using --ancestry-path for better reverse walking + if (options?.reverse) { + args.push('--reverse', '--ancestry-path', `${options.hash}..HEAD`); + } else { + args.push(options.hash); + } + } + + if (options?.sortByAuthorDate) { + args.push('--author-date-order'); + } + + args.push('--', uri.fsPath); const result = await this.run(args); if (result.exitCode) { diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 4f22b0784bb..4f1d21796ab 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -40,6 +40,29 @@ export const enum ResourceGroupType { export class Resource implements SourceControlResourceState { + static getStatusText(type: Status) { + switch (type) { + case Status.INDEX_MODIFIED: return localize('index modified', "Index Modified"); + case Status.MODIFIED: return localize('modified', "Modified"); + case Status.INDEX_ADDED: return localize('index added', "Index Added"); + case Status.INDEX_DELETED: return localize('index deleted', "Index Deleted"); + case Status.DELETED: return localize('deleted', "Deleted"); + case Status.INDEX_RENAMED: return localize('index renamed', "Index Renamed"); + case Status.INDEX_COPIED: return localize('index copied', "Index Copied"); + case Status.UNTRACKED: return localize('untracked', "Untracked"); + case Status.IGNORED: return localize('ignored', "Ignored"); + case Status.INTENT_TO_ADD: return localize('intent to add', "Intent to Add"); + case Status.BOTH_DELETED: return localize('both deleted', "Both Deleted"); + case Status.ADDED_BY_US: return localize('added by us', "Added By Us"); + case Status.DELETED_BY_THEM: return localize('deleted by them', "Deleted By Them"); + case Status.ADDED_BY_THEM: return localize('added by them', "Added By Them"); + case Status.DELETED_BY_US: return localize('deleted by us', "Deleted By Us"); + case Status.BOTH_ADDED: return localize('both added', "Both Added"); + case Status.BOTH_MODIFIED: return localize('both modified', "Both Modified"); + default: return ''; + } + } + @memoize get resourceUri(): Uri { if (this.renameResourceUri && (this._type === Status.MODIFIED || this._type === Status.DELETED || this._type === Status.INDEX_RENAMED || this._type === Status.INDEX_COPIED)) { @@ -110,26 +133,7 @@ export class Resource implements SourceControlResourceState { } private get tooltip(): string { - switch (this.type) { - case Status.INDEX_MODIFIED: return localize('index modified', "Index Modified"); - case Status.MODIFIED: return localize('modified', "Modified"); - case Status.INDEX_ADDED: return localize('index added', "Index Added"); - case Status.INDEX_DELETED: return localize('index deleted', "Index Deleted"); - case Status.DELETED: return localize('deleted', "Deleted"); - case Status.INDEX_RENAMED: return localize('index renamed', "Index Renamed"); - case Status.INDEX_COPIED: return localize('index copied', "Index Copied"); - case Status.UNTRACKED: return localize('untracked', "Untracked"); - case Status.IGNORED: return localize('ignored', "Ignored"); - case Status.INTENT_TO_ADD: return localize('intent to add', "Intent to Add"); - case Status.BOTH_DELETED: return localize('both deleted', "Both Deleted"); - case Status.ADDED_BY_US: return localize('added by us', "Added By Us"); - case Status.DELETED_BY_THEM: return localize('deleted by them', "Deleted By Them"); - case Status.ADDED_BY_THEM: return localize('added by them', "Added By Them"); - case Status.DELETED_BY_US: return localize('deleted by us', "Deleted By Us"); - case Status.BOTH_ADDED: return localize('both added', "Both Added"); - case Status.BOTH_MODIFIED: return localize('both modified', "Both Modified"); - default: return ''; - } + return Resource.getStatusText(this.type); } private get strikeThrough(): boolean { diff --git a/extensions/git/src/timelineProvider.ts b/extensions/git/src/timelineProvider.ts index 9db3495aec7..3fe8e306c95 100644 --- a/extensions/git/src/timelineProvider.ts +++ b/extensions/git/src/timelineProvider.ts @@ -3,17 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as nls from 'vscode-nls'; import * as dayjs from 'dayjs'; import * as advancedFormat from 'dayjs/plugin/advancedFormat'; -import { CancellationToken, Disposable, Event, EventEmitter, ThemeIcon, Timeline, TimelineChangeEvent, TimelineCursor, TimelineItem, TimelineProvider, Uri, workspace } from 'vscode'; +import { CancellationToken, Disposable, Event, EventEmitter, ThemeIcon, Timeline, TimelineChangeEvent, TimelineItem, TimelineOptions, TimelineProvider, Uri, workspace } from 'vscode'; import { Model } from './model'; -import { Repository } from './repository'; +import { Repository, Resource } from './repository'; import { debounce } from './decorators'; -import { Status } from './api/git'; dayjs.extend(advancedFormat); -// TODO[ECA]: Localize all the strings +const localize = nls.loadMessageBundle(); + // TODO[ECA]: Localize or use a setting for date format export class GitTimelineItem extends TimelineItem { @@ -68,7 +69,7 @@ export class GitTimelineProvider implements TimelineProvider { } readonly id = 'git-history'; - readonly label = 'Git History'; + readonly label = localize('git.timeline.source', 'Git History'); private _disposable: Disposable; @@ -87,7 +88,7 @@ export class GitTimelineProvider implements TimelineProvider { this._disposable.dispose(); } - async provideTimeline(uri: Uri, _cursor: TimelineCursor, _token: CancellationToken): Promise { + async provideTimeline(uri: Uri, options: TimelineOptions, _token: CancellationToken): Promise { // console.log(`GitTimelineProvider.provideTimeline: uri=${uri} state=${this._model.state}`); const repo = this._model.getRepository(uri); @@ -112,109 +113,110 @@ export class GitTimelineProvider implements TimelineProvider { // TODO[ECA]: Ensure that the uri is a file -- if not we could get the history of the repo? - const commits = await repo.logFile(uri); + let limit: number | undefined; + if (typeof options.limit === 'string') { + try { + const result = await this._model.git.exec(repo.root, ['rev-list', '--count', `${options.limit}..`, '--', uri.fsPath]); + if (!result.exitCode) { + // Ask for 1 more than so we can determine if there are more commits + limit = Number(result.stdout) + 1; + } + } + catch { + limit = undefined; + } + } else { + // If we are not getting everything, ask for 1 more than so we can determine if there are more commits + limit = options.limit === undefined ? undefined : options.limit + 1; + } + + + const commits = await repo.logFile(uri, { + maxEntries: limit, + hash: options.cursor, + reverse: options.before, + // sortByAuthorDate: true + }); + + const more = limit === undefined || options.before ? false : commits.length >= limit; + const paging = commits.length ? { + more: more, + cursors: { + before: commits[0]?.hash, + after: commits[commits.length - (more ? 1 : 2)]?.hash + } + } : undefined; + + // If we asked for an extra commit, strip it off + if (limit !== undefined && commits.length >= limit) { + commits.splice(commits.length - 1, 1); + } let dateFormatter: dayjs.Dayjs; const items = commits.map(c => { - dateFormatter = dayjs(c.authorDate); + const date = c.commitDate; // c.authorDate - const item = new GitTimelineItem(c.hash, `${c.hash}^`, c.message, c.authorDate?.getTime() ?? 0, c.hash, 'git:file:commit'); + dateFormatter = dayjs(date); + + const item = new GitTimelineItem(c.hash, `${c.hash}^`, c.message, date?.getTime() ?? 0, c.hash, 'git:file:commit'); item.iconPath = new (ThemeIcon as any)('git-commit'); item.description = c.authorName; item.detail = `${c.authorName} (${c.authorEmail}) \u2014 ${c.hash.substr(0, 8)}\n${dateFormatter.format('MMMM Do, YYYY h:mma')}\n\n${c.message}`; item.command = { title: 'Open Comparison', command: 'git.timeline.openDiff', - arguments: [uri, this.id, item] + arguments: [item, uri, this.id] }; return item; }); - const index = repo.indexGroup.resourceStates.find(r => r.resourceUri.fsPath === uri.fsPath); - if (index) { - const date = this._repoStatusDate ?? new Date(); - dateFormatter = dayjs(date); + if (options.cursor === undefined || options.before) { + const you = localize('git.timeline.you', 'You'); - let status; - switch (index.type) { - case Status.INDEX_MODIFIED: - status = 'Modified'; - break; - case Status.INDEX_ADDED: - status = 'Added'; - break; - case Status.INDEX_DELETED: - status = 'Deleted'; - break; - case Status.INDEX_RENAMED: - status = 'Renamed'; - break; - case Status.INDEX_COPIED: - status = 'Copied'; - break; - default: - status = ''; - break; + const index = repo.indexGroup.resourceStates.find(r => r.resourceUri.fsPath === uri.fsPath); + if (index) { + const date = this._repoStatusDate ?? new Date(); + dateFormatter = dayjs(date); + + const item = new GitTimelineItem('~', 'HEAD', localize('git.timeline.stagedChanges', 'Staged Changes'), date.getTime(), 'index', 'git:file:index'); + // TODO[ECA]: Replace with a better icon -- reflecting its status maybe? + item.iconPath = new (ThemeIcon as any)('git-commit'); + item.description = you; + item.detail = localize('git.timeline.detail', '{0} \u2014 {1}\n{2}\n\n{3}', you, localize('git.index', 'Index'), dateFormatter.format('MMMM Do, YYYY h:mma'), Resource.getStatusText(index.type)); + item.command = { + title: 'Open Comparison', + command: 'git.timeline.openDiff', + arguments: [item, uri, this.id] + }; + + items.splice(0, 0, item); } - const item = new GitTimelineItem('~', 'HEAD', 'Staged Changes', date.getTime(), 'index', 'git:file:index'); - // TODO[ECA]: Replace with a better icon -- reflecting its status maybe? - item.iconPath = new (ThemeIcon as any)('git-commit'); - item.description = 'You'; - item.detail = `You \u2014 Index\n${dateFormatter.format('MMMM Do, YYYY h:mma')}\n${status}`; - item.command = { - title: 'Open Comparison', - command: 'git.timeline.openDiff', - arguments: [uri, this.id, item] - }; + const working = repo.workingTreeGroup.resourceStates.find(r => r.resourceUri.fsPath === uri.fsPath); + if (working) { + const date = new Date(); + dateFormatter = dayjs(date); - items.push(item); - } + const item = new GitTimelineItem('', index ? '~' : 'HEAD', localize('git.timeline.uncommitedChanges', 'Uncommited Changes'), date.getTime(), 'working', 'git:file:working'); + // TODO[ECA]: Replace with a better icon -- reflecting its status maybe? + item.iconPath = new (ThemeIcon as any)('git-commit'); + item.description = you; + item.detail = localize('git.timeline.detail', '{0} \u2014 {1}\n{2}\n\n{3}', you, localize('git.workingTree', 'Working Tree'), dateFormatter.format('MMMM Do, YYYY h:mma'), Resource.getStatusText(working.type)); + item.command = { + title: 'Open Comparison', + command: 'git.timeline.openDiff', + arguments: [item, uri, this.id] + }; - - const working = repo.workingTreeGroup.resourceStates.find(r => r.resourceUri.fsPath === uri.fsPath); - if (working) { - const date = new Date(); - dateFormatter = dayjs(date); - - let status; - switch (working.type) { - case Status.INDEX_MODIFIED: - status = 'Modified'; - break; - case Status.INDEX_ADDED: - status = 'Added'; - break; - case Status.INDEX_DELETED: - status = 'Deleted'; - break; - case Status.INDEX_RENAMED: - status = 'Renamed'; - break; - case Status.INDEX_COPIED: - status = 'Copied'; - break; - default: - status = ''; - break; + items.splice(0, 0, item); } - - const item = new GitTimelineItem('', index ? '~' : 'HEAD', 'Uncommited Changes', date.getTime(), 'working', 'git:file:working'); - // TODO[ECA]: Replace with a better icon -- reflecting its status maybe? - item.iconPath = new (ThemeIcon as any)('git-commit'); - item.description = 'You'; - item.detail = `You \u2014 Working Tree\n${dateFormatter.format('MMMM Do, YYYY h:mma')}\n${status}`; - item.command = { - title: 'Open Comparison', - command: 'git.timeline.openDiff', - arguments: [uri, this.id, item] - }; - - items.push(item); } - return { items: items }; + return { + items: items, + paging: paging + }; } private onRepositoriesChanged(_repo: Repository) { @@ -241,6 +243,6 @@ export class GitTimelineProvider implements TimelineProvider { @debounce(500) private fireChanged() { - this._onDidChange.fire({}); + this._onDidChange.fire({ reset: true }); } } diff --git a/extensions/github-authentication/build/postinstall.js b/extensions/github-authentication/build/postinstall.js new file mode 100644 index 00000000000..e12fd05f191 --- /dev/null +++ b/extensions/github-authentication/build/postinstall.js @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +const fs = require('fs'); +const path = require('path'); + +const schemes = ['OSS', 'INSIDERS', 'STABLE', 'EXPLORATION', 'VSO', 'VSO_PPE', 'VSO_DEV']; + +function main() { + let content = {}; + + for (const scheme of schemes) { + const id = process.env[`${scheme}_GITHUB_ID`]; + const secret = process.env[`${scheme}_GITHUB_SECRET`]; + + if (id && secret) { + content[scheme] = { id, secret }; + } + } + + fs.writeFileSync(path.join(__dirname, '../src/common/config.json'), JSON.stringify(content)); +} + +main(); diff --git a/extensions/github-authentication/package.json b/extensions/github-authentication/package.json index 8060b63f2f5..254516ee527 100644 --- a/extensions/github-authentication/package.json +++ b/extensions/github-authentication/package.json @@ -1,32 +1,33 @@ { - "name": "github-authentication", - "displayName": "%displayName%", - "description": "%description%", - "publisher": "vscode", - "version": "0.0.1", - "engines": { - "vscode": "^1.41.0" - }, - "enableProposedApi": true, - "categories": [ - "Other" - ], - "activationEvents": [ - "*" - ], - "main": "./out/extension.js", - "scripts": { - "vscode:prepublish": "npm run compile", - "compile": "gulp compile-extension:github-authentication", - "watch": "gulp watch-extension:github-authentication" - }, - "dependencies": { - "uuid": "^3.3.3" - }, - "devDependencies": { - "@types/keytar": "^4.4.2", - "@types/node": "^10.12.21", - "@types/uuid": "^3.4.6", - "typescript": "^3.7.5" - } + "name": "github-authentication", + "displayName": "%displayName%", + "description": "%description%", + "publisher": "vscode", + "version": "0.0.1", + "engines": { + "vscode": "^1.41.0" + }, + "enableProposedApi": true, + "categories": [ + "Other" + ], + "activationEvents": [ + "*" + ], + "main": "./out/extension.js", + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "gulp compile-extension:github-authentication", + "watch": "gulp watch-extension:github-authentication", + "postinstall": "node build/postinstall.js" + }, + "dependencies": { + "uuid": "^3.3.3" + }, + "devDependencies": { + "@types/keytar": "^4.4.2", + "@types/node": "^10.12.21", + "@types/uuid": "^3.4.6", + "typescript": "^3.7.5" + } } diff --git a/extensions/github-authentication/src/common/clientRegistrar.ts b/extensions/github-authentication/src/common/clientRegistrar.ts index b4de0b8138a..c16c7221ff2 100644 --- a/extensions/github-authentication/src/common/clientRegistrar.ts +++ b/extensions/github-authentication/src/common/clientRegistrar.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Uri } from 'vscode'; + export interface ClientDetails { id?: string; secret?: string; @@ -11,6 +13,12 @@ export interface ClientDetails { export interface ClientConfig { OSS: ClientDetails; INSIDERS: ClientDetails; + STABLE: ClientDetails; + EXPLORATION: ClientDetails; + + VSO: ClientDetails; + VSO_PPE: ClientDetails; + VSO_DEV: ClientDetails; } export class Registrar { @@ -22,13 +30,18 @@ export class Registrar { } catch (e) { this._config = { OSS: {}, - INSIDERS: {} + INSIDERS: {}, + STABLE: {}, + EXPLORATION: {}, + VSO: {}, + VSO_PPE: {}, + VSO_DEV: {} }; } } - getClientDetails(product: string): ClientDetails { + getClientDetails(callbackUri: Uri): ClientDetails { let details: ClientDetails | undefined; - switch (product) { + switch (callbackUri.scheme) { case 'code-oss': details = this._config.OSS; break; @@ -37,12 +50,33 @@ export class Registrar { details = this._config.INSIDERS; break; + case 'vscode': + details = this._config.STABLE; + break; + + case 'vscode-exploration': + details = this._config.EXPLORATION; + break; + + case 'https': + switch (callbackUri.authority) { + case 'online.visualstudio.com': + details = this._config.VSO; + break; + case 'online-ppe.core.vsengsaas.visualstudio.com': + details = this._config.VSO_PPE; + break; + case 'online.dev.core.vsengsaas.visualstudio.com': + details = this._config.VSO_DEV; + break; + } + default: - throw new Error(`Unrecognized product ${product}`); + throw new Error(`Unrecognized callback ${callbackUri}`); } if (!details.id || !details.secret) { - throw new Error(`No client configuration available for ${product}`); + throw new Error(`No client configuration available for ${callbackUri}`); } return details; diff --git a/extensions/github-authentication/src/github.ts b/extensions/github-authentication/src/github.ts index fe4dcaa6d1f..0f46d20d81b 100644 --- a/extensions/github-authentication/src/github.ts +++ b/extensions/github-authentication/src/github.ts @@ -10,6 +10,13 @@ import Logger from './common/logger'; export const onDidChangeSessions = new vscode.EventEmitter(); +interface SessionData { + id: string; + accountName: string; + scopes: string[]; + accessToken: string; +} + export class GitHubAuthenticationProvider { private _sessions: vscode.AuthenticationSession[] = []; private _githubServer = new GitHubServer(); @@ -58,7 +65,15 @@ export class GitHubAuthenticationProvider { const storedSessions = await keychain.getToken(); if (storedSessions) { try { - return JSON.parse(storedSessions); + const sessionData: SessionData[] = JSON.parse(storedSessions); + return sessionData.map(session => { + return { + id: session.id, + accountName: session.accountName, + scopes: session.scopes, + accessToken: () => Promise.resolve(session.accessToken) + }; + }); } catch (e) { Logger.error(`Error reading sessions: ${e}`); } @@ -68,7 +83,17 @@ export class GitHubAuthenticationProvider { } private async storeSessions(): Promise { - await keychain.setToken(JSON.stringify(this._sessions)); + const sessionData: SessionData[] = await Promise.all(this._sessions.map(async session => { + const resolvedAccessToken = await session.accessToken(); + return { + id: session.id, + accountName: session.accountName, + scopes: session.scopes, + accessToken: resolvedAccessToken + }; + })); + + await keychain.setToken(JSON.stringify(sessionData)); } get sessions(): vscode.AuthenticationSession[] { diff --git a/extensions/github-authentication/src/githubServer.ts b/extensions/github-authentication/src/githubServer.ts index 6c84738261d..336a27b2392 100644 --- a/extensions/github-authentication/src/githubServer.ts +++ b/extensions/github-authentication/src/githubServer.ts @@ -71,7 +71,7 @@ export class GitHubServer { Logger.info('Logging in...'); const state = uuid(); const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/did-authenticate`)); - const clientDetails = ClientRegistrar.getClientDetails(callbackUri.scheme); + const clientDetails = ClientRegistrar.getClientDetails(callbackUri); const uri = vscode.Uri.parse(`https://github.com/login/oauth/authorize?redirect_uri=${encodeURIComponent(callbackUri.toString())}&scope=${scopes}&state=${state}&client_id=${clientDetails.id}`); vscode.env.openExternal(uri); diff --git a/extensions/html-language-features/package.json b/extensions/html-language-features/package.json index 526f54fcacb..00289a314a1 100644 --- a/extensions/html-language-features/package.json +++ b/extensions/html-language-features/package.json @@ -201,7 +201,7 @@ }, "dependencies": { "vscode-extension-telemetry": "0.1.1", - "vscode-languageclient": "^6.1.0", + "vscode-languageclient": "^6.1.1", "vscode-nls": "^4.1.1" }, "devDependencies": { diff --git a/extensions/html-language-features/server/package.json b/extensions/html-language-features/server/package.json index 0083f2b1bd5..5064259bfbf 100644 --- a/extensions/html-language-features/server/package.json +++ b/extensions/html-language-features/server/package.json @@ -9,14 +9,14 @@ }, "main": "./out/htmlServerMain", "dependencies": { - "vscode-css-languageservice": "^4.0.3-next.27", - "vscode-html-languageservice": "^3.0.4-next.14", - "vscode-languageserver": "^6.1.0", + "vscode-css-languageservice": "^4.1.0", + "vscode-html-languageservice": "3.0.4-next.15", + "vscode-languageserver": "^6.1.1", "vscode-nls": "^4.1.1", "vscode-uri": "^2.1.1" }, "devDependencies": { - "@types/mocha": "2.2.33", + "@types/mocha": "7.0.1", "@types/node": "^12.11.7", "glob": "^7.1.6", "mocha": "^7.0.1", diff --git a/extensions/html-language-features/server/yarn.lock b/extensions/html-language-features/server/yarn.lock index b2659f1c92d..f4ed7ba600d 100644 --- a/extensions/html-language-features/server/yarn.lock +++ b/extensions/html-language-features/server/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/mocha@2.2.33": - version "2.2.33" - resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-2.2.33.tgz#d79a0061ec270379f4d9e225f4096fb436669def" - integrity sha1-15oAYewnA3n02eIl9AlvtDZmne8= +"@types/mocha@7.0.1": + version "7.0.1" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-7.0.1.tgz#5d7ec2a789a1f77c59b7ad071b9d50bf1abbfc9e" + integrity sha512-L/Nw/2e5KUaprNJoRA33oly+M8X8n0K+FwLTbYqwTcR14wdPWeRkigBLfSFpN/Asf9ENZTMZwLxjtjeYucAA4Q== "@types/node@^12.11.7": version "12.11.7" @@ -714,20 +714,20 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -vscode-css-languageservice@^4.0.3-next.27: - version "4.0.3-next.27" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.0.3-next.27.tgz#04f52bfdd85ac7eed8d82a0133aaabb4c0e18f7f" - integrity sha512-MU8sHQABb1WnzOwgazIdO4lXG7cGzFKd7VKi5j67uWTNsqSrbAVSoKGjSyOLq/o6h1L5DGG1Og/7q403z6D04g== +vscode-css-languageservice@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.1.0.tgz#144c8274e0bf1719fa6f773ca684bd1c7ffd634f" + integrity sha512-iTX3dTp0Y0RFWhIux5jasI8r9swdiWVB1Z3OrZ10iDHxzkETjVPxAQ5BEQU4ag0Awc8TTg1C7sJriHQY2LO14g== dependencies: - vscode-languageserver-textdocument "^1.0.1-next.1" + vscode-languageserver-textdocument "^1.0.1" vscode-languageserver-types "^3.15.1" vscode-nls "^4.1.1" vscode-uri "^2.1.1" -vscode-html-languageservice@^3.0.4-next.14: - version "3.0.4-next.14" - resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-3.0.4-next.14.tgz#143cda8d1b032da3506de2ee7c352f50159edc7f" - integrity sha512-YQ1nYy3Yqai+mrvORFeJW1/CvkBd9y2kYDOvfTEmt5uCd7+7iMTQLdhPFWwlx/GKDXuvQJoJ0E3eNd/gGb3cvQ== +vscode-html-languageservice@3.0.4-next.15: + version "3.0.4-next.15" + resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-3.0.4-next.15.tgz#7214ccd9b4a06cf138b5945d9fd88285a0add490" + integrity sha512-UmUm3A1ZTj+BloVIyel+5pK/nfsqRfPLXzl8BA9O7v5Cj64vivddABvNf/rW1US8fzdikFNZNloC/4ooqxB2kw== dependencies: vscode-languageserver-textdocument "^1.0.1-next.1" vscode-languageserver-types "^3.15.1" @@ -739,14 +739,19 @@ vscode-jsonrpc@^5.0.1: resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz#9bab9c330d89f43fc8c1e8702b5c36e058a01794" integrity sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A== -vscode-languageserver-protocol@^3.15.2: - version "3.15.2" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.2.tgz#e52c62923140b2655ad2472f6f29cfb83bacf5b8" - integrity sha512-GdL05JKOgZ76RDg3suiGCl9enESM7iQgGw4x93ibTh4sldvZmakHmTeZ4iUApPPGKf6O3OVBtrsksBXnHYaxNg== +vscode-languageserver-protocol@^3.15.3: + version "3.15.3" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.3.tgz#3fa9a0702d742cf7883cb6182a6212fcd0a1d8bb" + integrity sha512-zrMuwHOAQRhjDSnflWdJG+O2ztMWss8GqUUB8dXLR/FPenwkiBNkMIJJYfSN6sgskvsF0rHAoBowNQfbyZnnvw== dependencies: vscode-jsonrpc "^5.0.1" vscode-languageserver-types "3.15.1" +vscode-languageserver-textdocument@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.1.tgz#178168e87efad6171b372add1dea34f53e5d330f" + integrity sha512-UIcJDjX7IFkck7cSkNNyzIz5FyvpQfY7sdzVy+wkKN/BLaD4DQ0ppXQrKePomCxTS7RrolK1I0pey0bG9eh8dA== + vscode-languageserver-textdocument@^1.0.1-next.1: version "1.0.1-next.1" resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.1-next.1.tgz#c8f2f792c7c88d33ea8441ca04bfb8376896b671" @@ -757,12 +762,12 @@ vscode-languageserver-types@3.15.1, vscode-languageserver-types@^3.15.1: resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz#17be71d78d2f6236d414f0001ce1ef4d23e6b6de" integrity sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ== -vscode-languageserver@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-6.1.0.tgz#f0ff149b06d1961f49ed03385ecc2a96bcaddcde" - integrity sha512-Q5kUJegYclTZMnKUaEcxJK41Ozp6qJhhoFJYj0w8y8j9JXdKT479LE945QCKRvSgWfsqTSUmgsozVTUIwQQxHw== +vscode-languageserver@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-6.1.1.tgz#d76afc68172c27d4327ee74332b468fbc740d762" + integrity sha512-DueEpkUAkD5XTR4MLYNr6bQIp/UFR0/IPApgXU3YfCBCB08u2sm9hRCs6DxYZELkk++STPjpcjksR2H8qI3cDQ== dependencies: - vscode-languageserver-protocol "^3.15.2" + vscode-languageserver-protocol "^3.15.3" vscode-nls@^4.1.1: version "4.1.1" diff --git a/extensions/html-language-features/yarn.lock b/extensions/html-language-features/yarn.lock index e91ad63907d..8dfd3e7e9d0 100644 --- a/extensions/html-language-features/yarn.lock +++ b/extensions/html-language-features/yarn.lock @@ -50,18 +50,18 @@ vscode-jsonrpc@^5.0.1: resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz#9bab9c330d89f43fc8c1e8702b5c36e058a01794" integrity sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A== -vscode-languageclient@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-6.1.0.tgz#ee67c0b7818c42ce0281572d05c89adfcc4f5a38" - integrity sha512-Tcp0VoOaa0YzxL4nEfK9tsmcy76Eo8jNLvFQZwh2c8oMm02luL8uGYPLQNAiZ3XGgegfcwiQFZMqbW7DNV0vxA== +vscode-languageclient@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-6.1.1.tgz#91b62e416c5abbf2013ae3726f314a19c22a8457" + integrity sha512-mB6d8Tg+82l8EFUfR+SBu0+lCshyKVgC5E5+MQ0/BJa+9AgeBjtG5npoGaCo4/VvWzK0ZRGm85zU5iRp1RYPIA== dependencies: semver "^6.3.0" - vscode-languageserver-protocol "^3.15.2" + vscode-languageserver-protocol "^3.15.3" -vscode-languageserver-protocol@^3.15.2: - version "3.15.2" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.2.tgz#e52c62923140b2655ad2472f6f29cfb83bacf5b8" - integrity sha512-GdL05JKOgZ76RDg3suiGCl9enESM7iQgGw4x93ibTh4sldvZmakHmTeZ4iUApPPGKf6O3OVBtrsksBXnHYaxNg== +vscode-languageserver-protocol@^3.15.3: + version "3.15.3" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.3.tgz#3fa9a0702d742cf7883cb6182a6212fcd0a1d8bb" + integrity sha512-zrMuwHOAQRhjDSnflWdJG+O2ztMWss8GqUUB8dXLR/FPenwkiBNkMIJJYfSN6sgskvsF0rHAoBowNQfbyZnnvw== dependencies: vscode-jsonrpc "^5.0.1" vscode-languageserver-types "3.15.1" diff --git a/extensions/image-preview/package.json b/extensions/image-preview/package.json index a0664f98e3d..48c7ae314a9 100644 --- a/extensions/image-preview/package.json +++ b/extensions/image-preview/package.json @@ -17,15 +17,15 @@ "Other" ], "activationEvents": [ - "onWebviewEditor:imagePreview.previewEditor", + "onCustomEditor:imagePreview.previewEditor", "onCommand:imagePreview.zoomIn", "onCommand:imagePreview.zoomOut" ], "contributes": { - "webviewEditors": [ + "customEditors": [ { "viewType": "imagePreview.previewEditor", - "displayName": "%webviewEditors.displayName%", + "displayName": "%customEditors.displayName%", "priority": "builtin", "selector": [ { diff --git a/extensions/image-preview/package.nls.json b/extensions/image-preview/package.nls.json index 304b1df9a3d..d1860bc2fb5 100644 --- a/extensions/image-preview/package.nls.json +++ b/extensions/image-preview/package.nls.json @@ -1,7 +1,7 @@ { "displayName": "Image Preview", "description": "Provides VS Code's built-in image preview", - "webviewEditors.displayName": "Image Preview", + "customEditors.displayName": "Image Preview", "command.zoomIn": "Zoom in", "command.zoomOut": "Zoom out" } diff --git a/extensions/image-preview/src/extension.ts b/extensions/image-preview/src/extension.ts index dc0d5c33956..cbb487e094a 100644 --- a/extensions/image-preview/src/extension.ts +++ b/extensions/image-preview/src/extension.ts @@ -23,7 +23,7 @@ export function activate(context: vscode.ExtensionContext) { const previewManager = new PreviewManager(extensionRoot, sizeStatusBarEntry, binarySizeStatusBarEntry, zoomStatusBarEntry); - context.subscriptions.push(vscode.window.registerWebviewCustomEditorProvider(PreviewManager.viewType, previewManager)); + context.subscriptions.push(vscode.window.registerCustomEditorProvider(PreviewManager.viewType, previewManager)); context.subscriptions.push(vscode.commands.registerCommand('imagePreview.zoomIn', () => { previewManager.activePreview?.zoomIn(); diff --git a/extensions/image-preview/src/preview.ts b/extensions/image-preview/src/preview.ts index 5e733b13cc2..fc43a136a01 100644 --- a/extensions/image-preview/src/preview.ts +++ b/extensions/image-preview/src/preview.ts @@ -13,7 +13,7 @@ import { BinarySizeStatusBarEntry } from './binarySizeStatusBarEntry'; const localize = nls.loadMessageBundle(); -export class PreviewManager implements vscode.WebviewCustomEditorProvider { +export class PreviewManager implements vscode.CustomEditorProvider { public static readonly viewType = 'imagePreview.previewEditor'; @@ -27,11 +27,15 @@ export class PreviewManager implements vscode.WebviewCustomEditorProvider { private readonly zoomStatusBarEntry: ZoomStatusBarEntry, ) { } - public async resolveWebviewEditor( - resource: vscode.Uri, + public async resolveCustomDocument(_document: vscode.CustomDocument): Promise { + return {}; + } + + public async resolveCustomEditor( + document: vscode.CustomDocument, webviewEditor: vscode.WebviewPanel, ): Promise { - const preview = new Preview(this.extensionRoot, resource, webviewEditor, this.sizeStatusBarEntry, this.binarySizeStatusBarEntry, this.zoomStatusBarEntry); + const preview = new Preview(this.extensionRoot, document.uri, webviewEditor, this.sizeStatusBarEntry, this.binarySizeStatusBarEntry, this.zoomStatusBarEntry); this._previews.add(preview); this.setActivePreview(preview); diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index fa3f06ca1cf..dbc8746c913 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -26,7 +26,7 @@ "onCommand:markdown.showPreviewSecuritySelector", "onCommand:markdown.api.render", "onWebviewPanel:markdown.preview", - "onWebviewEditor:vscode.markdown.preview.editor" + "onCustomEditor:vscode.markdown.preview.editor" ], "contributes": { "commands": [ @@ -309,7 +309,7 @@ "markdown.previewScripts": [ "./media/index.js" ], - "webviewEditors": [ + "customEditors": [ { "viewType": "vscode.markdown.preview.editor", "displayName": "(Experimental) VS Code Markdown Preview", diff --git a/extensions/markdown-language-features/src/features/previewManager.ts b/extensions/markdown-language-features/src/features/previewManager.ts index 333a0651a44..68d68118309 100644 --- a/extensions/markdown-language-features/src/features/previewManager.ts +++ b/extensions/markdown-language-features/src/features/previewManager.ts @@ -52,7 +52,7 @@ class PreviewStore extends Disposable { } } -export class MarkdownPreviewManager extends Disposable implements vscode.WebviewPanelSerializer, vscode.WebviewCustomEditorProvider { +export class MarkdownPreviewManager extends Disposable implements vscode.WebviewPanelSerializer, vscode.CustomEditorProvider { private static readonly markdownPreviewActiveContextKey = 'markdownPreviewFocus'; private readonly _topmostLineMonitor = new TopmostLineMonitor(); @@ -70,7 +70,7 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview ) { super(); this._register(vscode.window.registerWebviewPanelSerializer(DynamicMarkdownPreview.viewType, this)); - this._register(vscode.window.registerWebviewCustomEditorProvider('vscode.markdown.preview.editor', this)); + this._register(vscode.window.registerCustomEditorProvider('vscode.markdown.preview.editor', this)); } public refresh() { @@ -148,12 +148,16 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview this.registerDynamicPreview(preview); } - public async resolveWebviewEditor( - resource: vscode.Uri, + public async resolveCustomDocument(_document: vscode.CustomDocument): Promise { + return {}; + } + + public async resolveCustomEditor( + document: vscode.CustomDocument, webview: vscode.WebviewPanel ): Promise { const preview = DynamicMarkdownPreview.revive( - { resource, locked: false, resourceColumn: vscode.ViewColumn.One }, + { resource: document.uri, locked: false, resourceColumn: vscode.ViewColumn.One }, webview, this._contentProvider, this._previewConfigurations, diff --git a/extensions/npm/package.json b/extensions/npm/package.json index cbd9645341a..79a95f7fc1b 100644 --- a/extensions/npm/package.json +++ b/extensions/npm/package.json @@ -18,10 +18,10 @@ "watch": "gulp watch-extension:npm" }, "dependencies": { - "jsonc-parser": "^2.1.1", + "jsonc-parser": "^2.2.1", "minimatch": "^3.0.4", "request-light": "^0.2.5", - "vscode-nls": "^4.0.0" + "vscode-nls": "^4.1.1" }, "devDependencies": { "@types/minimatch": "^3.0.3", diff --git a/extensions/npm/yarn.lock b/extensions/npm/yarn.lock index 4c6c723c3cb..9b8a23c9264 100644 --- a/extensions/npm/yarn.lock +++ b/extensions/npm/yarn.lock @@ -79,10 +79,10 @@ https-proxy-agent@^2.2.3: agent-base "^4.3.0" debug "^3.1.0" -jsonc-parser@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.1.1.tgz#83dc3d7a6e7186346b889b1280eefa04446c6d3e" - integrity sha512-VC0CjnWJylKB1iov4u76/W/5Ef0ydDkjtYWxoZ9t3HdWlSnZQwZL5MgFikaB/EtQ4RmMEw3tmQzuYnZA2/Ja1g== +jsonc-parser@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.2.1.tgz#db73cd59d78cce28723199466b2a03d1be1df2bc" + integrity sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w== minimatch@^3.0.4: version "3.0.4" @@ -105,11 +105,6 @@ request-light@^0.2.5: https-proxy-agent "^2.2.3" vscode-nls "^4.1.1" -vscode-nls@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.0.0.tgz#4001c8a6caba5cedb23a9c5ce1090395c0e44002" - integrity sha512-qCfdzcH+0LgQnBpZA53bA32kzp9rpq/f66Som577ObeuDlFIrtbEJ+A/+CCxjIh4G8dpJYNCKIsxpRAHIfsbNw== - vscode-nls@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.1.tgz#f9916b64e4947b20322defb1e676a495861f133c" diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index e1f96f66c6d..7392a267a56 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -16,12 +16,12 @@ "Programming Languages" ], "dependencies": { - "jsonc-parser": "^2.1.1", + "jsonc-parser": "^2.2.1", "rimraf": "^2.6.3", "semver": "5.5.1", "typescript-vscode-sh-plugin": "^0.6.8", "vscode-extension-telemetry": "0.1.1", - "vscode-nls": "^4.0.0" + "vscode-nls": "^4.1.1" }, "devDependencies": { "@types/node": "^12.11.7", diff --git a/extensions/typescript-language-features/src/features/completions.ts b/extensions/typescript-language-features/src/features/completions.ts index 52e82adf2a8..e2f8ba9fd37 100644 --- a/extensions/typescript-language-features/src/features/completions.ts +++ b/extensions/typescript-language-features/src/features/completions.ts @@ -83,7 +83,15 @@ class MyCompletionItem extends vscode.CompletionItem { if (completionContext.isMemberCompletion && completionContext.dotAccessorContext) { this.filterText = completionContext.dotAccessorContext.text + (this.insertText || this.label); if (!this.range) { - this.range = completionContext.dotAccessorContext.range; + const replacementRange = this.getReplaceRange(line); + if (replacementRange) { + this.range = { + inserting: completionContext.dotAccessorContext.range, + replacing: completionContext.dotAccessorContext.range.union(replacementRange), + }; + } else { + this.range = completionContext.dotAccessorContext.range; + } this.insertText = this.filterText; } } @@ -135,7 +143,6 @@ class MyCompletionItem extends vscode.CompletionItem { } else { return wordStart === '#' ? undefined : this.tsEntry.name.replace(/^#/, ''); } - return undefined; } // For `this.` completions, generally don't set the filter text since we don't want them to be overly prioritized. #74164 @@ -162,6 +169,16 @@ class MyCompletionItem extends vscode.CompletionItem { return; } + const replaceRange = this.getReplaceRange(line); + if (replaceRange) { + this.range = { + inserting: new vscode.Range(replaceRange.start, this.position), + replacing: replaceRange + }; + } + } + + private getReplaceRange(line: string) { const wordRange = this.document.getWordRangeAtPosition(this.position); let replaceRange = wordRange; @@ -177,12 +194,7 @@ class MyCompletionItem extends vscode.CompletionItem { } } - if (replaceRange) { - this.range = { - inserting: new vscode.Range(replaceRange.start, this.position), - replacing: replaceRange - }; - } + return replaceRange; } private static convertKind(kind: string): vscode.CompletionItemKind { diff --git a/extensions/typescript-language-features/src/test/completions.test.ts b/extensions/typescript-language-features/src/test/completions.test.ts index 20afd43678e..0c928fb4a39 100644 --- a/extensions/typescript-language-features/src/test/completions.test.ts +++ b/extensions/typescript-language-features/src/test/completions.test.ts @@ -622,4 +622,29 @@ suite('TypeScript Completions', () => { `Config: ${config}`); }); }); + + test('Replace should work after this. (#91105)', async () => { + await updateConfig(testDocumentUri, { [Config.insertMode]: 'replace' }); + + const editor = await createTestEditor(testDocumentUri, + `class A {`, + ` abc = 1`, + ` foo() {`, + ` this.$0abc`, + ` }`, + `}`, + ); + + await acceptFirstSuggestion(testDocumentUri, _disposables); + + assertEditorContents(editor, + joinLines( + `class A {`, + ` abc = 1`, + ` foo() {`, + ` this.abc`, + ` }`, + `}`, + )); + }); }); diff --git a/extensions/typescript-language-features/src/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index 6be28453e0e..2d256281438 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -109,6 +109,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType private lastStart: number; private numberRestarts: number; private isRestarting: boolean = false; + private hasServerFatallyCrashedTooManyTimes = false; private readonly loadingIndicator = new ServerInitializingIndicator(); public readonly telemetryReporter: TelemetryReporter; @@ -306,7 +307,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType private token: number = 0; private startService(resendModels: boolean = false): ServerState.State { - if (this.isDisposed) { + if (this.isDisposed || this.hasServerFatallyCrashedTooManyTimes) { return ServerState.None; } @@ -477,7 +478,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType this.bufferSyncSupport.reset(); const watchOptions = this.apiVersion.gte(API.v380) - ? vscode.workspace.getConfiguration('typescript').get('tsserver.watchOptions') + ? this.configuration.watchOptions : undefined; const configureOptions: Proto.ConfigureRequestArguments = { @@ -542,6 +543,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType if (diff < 10 * 1000 /* 10 seconds */) { this.lastStart = Date.now(); startService = false; + this.hasServerFatallyCrashedTooManyTimes = true; prompt = vscode.window.showErrorMessage( localize('serverDiedAfterStart', 'The TypeScript language service died 5 times right after it got started. The service will not be restarted.'), { diff --git a/extensions/typescript-language-features/src/utils/configuration.ts b/extensions/typescript-language-features/src/utils/configuration.ts index 25315feab3c..f42b0c780b2 100644 --- a/extensions/typescript-language-features/src/utils/configuration.ts +++ b/extensions/typescript-language-features/src/utils/configuration.ts @@ -2,10 +2,12 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as vscode from 'vscode'; -import * as arrays from './arrays'; + import * as os from 'os'; import * as path from 'path'; +import * as vscode from 'vscode'; +import * as objects from '../utils/objects'; +import * as arrays from './arrays'; export enum TsServerLogLevel { Off, @@ -50,13 +52,14 @@ export class TypeScriptServiceConfiguration { public readonly localTsdk: string | null; public readonly npmLocation: string | null; public readonly tsServerLogLevel: TsServerLogLevel = TsServerLogLevel.Off; - public readonly tsServerPluginPaths: string[]; + public readonly tsServerPluginPaths: readonly string[]; public readonly checkJs: boolean; public readonly experimentalDecorators: boolean; public readonly disableAutomaticTypeAcquisition: boolean; public readonly useSeparateSyntaxServer: boolean; public readonly enableProjectDiagnostics: boolean; public readonly maxTsServerMemory: number; + public readonly watchOptions: protocol.WatchOptions | undefined; public static loadFromWorkspace(): TypeScriptServiceConfiguration { return new TypeScriptServiceConfiguration(); @@ -77,6 +80,7 @@ export class TypeScriptServiceConfiguration { this.useSeparateSyntaxServer = TypeScriptServiceConfiguration.readUseSeparateSyntaxServer(configuration); this.enableProjectDiagnostics = TypeScriptServiceConfiguration.readEnableProjectDiagnostics(configuration); this.maxTsServerMemory = TypeScriptServiceConfiguration.readMaxTsServerMemory(configuration); + this.watchOptions = TypeScriptServiceConfiguration.readWatchOptions(configuration); } public isEqualTo(other: TypeScriptServiceConfiguration): boolean { @@ -91,7 +95,8 @@ export class TypeScriptServiceConfiguration { && arrays.equals(this.tsServerPluginPaths, other.tsServerPluginPaths) && this.useSeparateSyntaxServer === other.useSeparateSyntaxServer && this.enableProjectDiagnostics === other.enableProjectDiagnostics - && this.maxTsServerMemory === other.maxTsServerMemory; + && this.maxTsServerMemory === other.maxTsServerMemory + && objects.equals(this.watchOptions, other.watchOptions); } private static fixPathPrefixes(inspectValue: string): string { @@ -157,6 +162,10 @@ export class TypeScriptServiceConfiguration { return configuration.get('typescript.tsserver.experimental.enableProjectDiagnostics', false); } + private static readWatchOptions(configuration: vscode.WorkspaceConfiguration): protocol.WatchOptions | undefined { + return configuration.get('typescript.tsserver.watchOptions'); + } + private static readMaxTsServerMemory(configuration: vscode.WorkspaceConfiguration): number { const defaultMaxMemory = 3072; const minimumMaxMemory = 128; diff --git a/extensions/typescript-language-features/src/utils/objects.ts b/extensions/typescript-language-features/src/utils/objects.ts new file mode 100644 index 00000000000..a31467bd8d6 --- /dev/null +++ b/extensions/typescript-language-features/src/utils/objects.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as array from './arrays'; + +export function equals(one: any, other: any): boolean { + if (one === other) { + return true; + } + if (one === null || one === undefined || other === null || other === undefined) { + return false; + } + if (typeof one !== typeof other) { + return false; + } + if (typeof one !== 'object') { + return false; + } + if (Array.isArray(one) !== Array.isArray(other)) { + return false; + } + + if (Array.isArray(one)) { + return array.equals(one, other, equals); + } else { + const oneKeys: string[] = []; + for (const key in one) { + oneKeys.push(key); + } + oneKeys.sort(); + const otherKeys: string[] = []; + for (const key in other) { + otherKeys.push(key); + } + otherKeys.sort(); + if (!array.equals(oneKeys, otherKeys)) { + return false; + } + return oneKeys.every(key => equals(one[key], other[key])); + } +} diff --git a/extensions/typescript-language-features/yarn.lock b/extensions/typescript-language-features/yarn.lock index c84c7d75280..82daf1829dd 100644 --- a/extensions/typescript-language-features/yarn.lock +++ b/extensions/typescript-language-features/yarn.lock @@ -393,10 +393,10 @@ json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= -jsonc-parser@^2.1.1: - version "2.2.0" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.2.0.tgz#f206f87f9d49d644b7502052c04e82dd6392e9ef" - integrity sha512-4fLQxW1j/5fWj6p78vAlAafoCKtuBm6ghv+Ij5W2DrDx0qE+ZdEl2c6Ko1mgJNF5ftX1iEWQQ4Ap7+3GlhjkOA== +jsonc-parser@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.2.1.tgz#db73cd59d78cce28723199466b2a03d1be1df2bc" + integrity sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w== jsprim@^1.2.2: version "1.4.1" @@ -667,7 +667,7 @@ vscode-extension-telemetry@0.1.1: dependencies: applicationinsights "1.0.8" -vscode-nls@^4.0.0: +vscode-nls@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.1.tgz#f9916b64e4947b20322defb1e676a495861f133c" integrity sha512-4R+2UoUUU/LdnMnFjePxfLqNhBS8lrAFyX7pjb2ud/lqDkrUavFUTcG7wR0HBZFakae0Q6KLBFjMS6W93F403A== diff --git a/extensions/vscode-account/media/auth.css b/extensions/vscode-account/media/auth.css index e87a6372763..45c42c75ad5 100644 --- a/extensions/vscode-account/media/auth.css +++ b/extensions/vscode-account/media/auth.css @@ -16,14 +16,14 @@ body { flex-direction: column; color: white; font-family: "Segoe UI","Helvetica Neue","Helvetica",Arial,sans-serif; - background-color: #373277; + background-color: #2C2C32; } .branding { - background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PGRlZnM+PHN0eWxlPi5pY29uLWNhbnZhcy10cmFuc3BhcmVudHtmaWxsOiNmNmY2ZjY7b3BhY2l0eTowO30uaWNvbi13aGl0ZXtmaWxsOiNmZmY7fTwvc3R5bGU+PC9kZWZzPjx0aXRsZT5CcmFuZFZpc3VhbFN0dWRpb0NvZGUyMDE3UlRXXzI0eF93aGl0ZV8yNHg8L3RpdGxlPjxwYXRoIGNsYXNzPSJpY29uLWNhbnZhcy10cmFuc3BhcmVudCIgZD0iTTI0LDBWMjRIMFYwWiIvPjxwYXRoIGNsYXNzPSJpY29uLXdoaXRlIiBkPSJNMjQsMi41VjIxLjVMMTgsMjQsMCwxOC41di0uNTYxbDE4LDEuNTQ1VjBaTTEsMTMuMTExLDQuMzg1LDEwLDEsNi44ODlsMS40MTgtLjgyN0w1Ljg1Myw4LjY1LDEyLDNsMywxLjQ1NlYxNS41NDRMMTIsMTcsNS44NTMsMTEuMzUsMi40MTksMTMuOTM5Wk03LjY0NCwxMCwxMiwxMy4yODNWNi43MTdaIi8+PC9zdmc+"); + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAACWCAYAAAA8AXHiAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAEgAAAABAAAASAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAlqADAAQAAAABAAAAlgAAAADkcSUjAAAACXBIWXMAAAsTAAALEwEAmpwYAAABWWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS40LjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgpMwidZAAAxaElEQVR4Ae19CbgdRbXu6j2cecoECTIkICCGzAg+7qeQ9544QFQgiXpVEJTEe59ALsbMwE5AMZCQELgKeSoqDlyiQogCSUAC6FNCQhIwQMALCbNMGc68p37/v7prnz47++yzzzl76OCu851d3dXV1VXVf69atWqtVZaUg397IPJwSCKT46zgx16wh+19XSYnO6PjE7H4MDuZ7BSxXhQJ/On5KTWbTSMm3bYlvHXmJNxj2SatFLFVioeWn5lDDwAgMvOU2NG/3DMo2h691rLtr4bqBtVX1NVJRUOVBIIiyYSI3RkXO9q5NZmU5S98tu7XpmQF2AwAzCoNwMrAMm/CT3HkrgqJTI/KwodOD4ZDa5MVdUPtfe+KxONKiSob6+SIE46XqsYGK5FIBoLVtQEJiSRaWp+xktaNuzpqfirTLcBOZNJtdnjrDCk6wMrA8hOgWJcZoFSrT4nJogfPESu4Dv8AVLTTEqsC9MkSDHB2NIYfkZGnjJOqpiZQrkTCAmWyKqtDgXBAEs0tLyZtWRl7953/u/uiUR0sVgH2xuKERCJJnhc6lIFV6B7OuXyAJrIpqDzVlQ99TUKVt0scAEomQaXskBAONlEFrgpvLdkZk6r6Wjn6IxMx2qVeY0IsSVrhqnCgMiiJA62v4a5Vkmz/4a7PDWtmVXSIfGMSAGYVFGCpGvGh5VCiHohEAiJXi77sKx+eLZU1N0hHCyuTAJCCDqCAKIUCYxv4AcY6YnL0qeOkZuhQSeoo6b5OG+AC4ZJQRThQFQbAWt7Bvf8ZSNg3P3deA8ZUF2AzADCrMAArA4u9XMoQsQMp6nHlH5dKdcMcad+fJGVCCKRAxfMU1XKB1dYpI8acKI0jj5FkDNSti3LpzSgjCQQmrFA4HKiuIMAO2EnrtkBb+8pdXxr2OjOd+bAd2vQ2cro8mXPjwH/LwBp4H/a/hLvuCsr06cpky6KHfiK1jRdJ235M80ClABMtmMMfKJQDNB4j1VCstg4ZcTKANWpkZmCxAJZiABYIhgO1VRwi21HGj63OxI27pje9xGz5BlgZWOzVUgSPjEqufOheqWmcIq37YwBCWKsDDGlQULnAUh6L4CLFAh/fCmCNOSE7sNxi3IilxiUQCAdrayR+oCWBcn4q0eSy56c2PMc80+6yg2t4MEAKVgYWO7HYwcz8Ig9XSTz5kNQ0nC7tB6KoBmZ+bnAZdaVUhIMHVDxWQtQCYI3thWKZ8rrHLICiCwCslmIKFGn/GoPm9S+cW7ddswJgZw4Ta9NkC/n6HsrA6nufDewOV/Ap8x8cggHvMUztTpLO5igkCV2g4hMIJI3dY56mqBdwgXOlWP0DlhbNkhVgtoQDELza7Z1iJ2J3J2P2DX8/r+Evmsm2A2dukkBfAYbZSDkUrQco+IQ0XeY9eKwEraekovYkzP4OBhUrlGLEXYB1q6QnzXPYLUtuJ5SLcei1k62tMYDKDtTWnRusrf5/J9zbcv9xa5snc9aooLJti3xYbsVy1lEOxekBI01f8NBECVnbJVx1hHS2YSrnGf4y1SQTwAyYTJzpvr6lAWA2AWYlW1vidrQjGait/VS4pvaPx9/bvOn43+4/GwCzDcAoC0P+rKNd1ot9q1s5d489YIa/BQ9+Ap/yAxKsgJSJ8gGXUc94I1BD4Og/j91zDodYGFTmvf88VsYnpiWSt7ICNXWcoQoo2uNJ217298/X/8bky7ZcVKZYppcKE0OaDg0FDn9XPfwFCYc3YEYGUEUpTQeFUNTgyZniDBUyfJdecu9hVJjAYS+YbGuJ8z9QXXNaqL5uzQlrm3d88O79X+Ujt860MIu17ExDZJliFealkMJYMn1NQNZATnXlQ9+ScPXNEm3H02zKrZQK9PpoxQ5+PBJ3pVwqbkBJpFhjPiSNx47sWY7V60Nyy4BaQDQhdqCyJgRWX5LNLbuA8xUfaKz7sTtEkkhhMuBoU5QpVm792rdclKbzkyWoFj64WCrrAKo2SNMxhukSjb4CvoZe/pGBebyfP88ZTNx14KQX6BdV4McQSna2JZItLXGrsurEUEPdra8daPnv43934CIy+Qoqth2hDKx8v4hpkKbrAi++3AUbf4AlmqukvTmuiytgWfr3OBdg/bs533cpwMDgJ6BFEQtUVB4dGlz/kxPuab5TH8S2A1z9bGi+6/o+KY/8FKkUw6KH/ktqm/4Ngk8y6UHltvVCH39IrQx1SsXmoI9l5Tc72xS2Y50AWGtnaEjdF46/p3mtPuJqqmOUQ356YMZtYai8xITrfzsGb4Dg8392W6IZ8FOygImXslwe8KOzF0Dti2D8vZbO0KC6z37wdwfm/d2yvu8dvbPfXr7acw8YcULkrw0Sa34EoBovHZCmq3Jez7dlvwKkGMCoqIHn+E8tQtse5n1UwZn37HXVq0nOeO14sj0Ws44tD4U59FjWLEaaPue+IyXWskMqa11QUfCZQgaK6OtxhqeaIkpInjLUyiRBjJKMhRrrqkOh5IXlodB0S3/ilDR9/cngyx+RcOVg6WjtXZqe67OUUqVnJroQsgu+nTzF/oXUlsIUTIk/WaZY/e18A6r5D34MXbkV0vTBEoVKZ1Zpen8flv2+FCHLnq0YV4N2Zwcwb48pU6z+dDfVXiKnRKGh8HksJt+tIp44penZlmj68yBzD6DjRQ8pmQbwWR4u2aSau0oQg2JRT9EaUqZYfep9vEaKFGhFs2DDN6Si8m594Yk4RQyF+0gVMRlgwyTzb9qRIZu51C02+Uzc7WLaSS55eIvJh1WHwnVGWt0O+VM1eFgMg4dIXBY8OF+qar8HlRd0JciHRRutEoXUy/Q836R5kjIemnwmzpjJTcwlD7O6+crAytaZ5ppK013B5/wNy6W6/goIPkmlMBD1V5puCu8h5gs66GV6E7zHPZRRsmSrgOS7ZI3K84O9Bg/zN/wcuulfVYMHZ+3Mw+Hk+bmmuHT8pPgrZki/aG4qfVymWNneAfmp6Y5TDpm34T6s+306v9L0bA/ntSzAMZdK45qht4qXKVaPPeTM/GISWVcjnZV/xPB3mho8WL1ofPZY4AAuGHmWAZMpKv3cpPsgLlOsTC/BLNHMXn+YRAOPglE/ERoK3a1oMt1XrDQdDsne4YEEl/kv1vOzPcetUxlY6Z3kLNFEZdGG42Gk/hh00w9Xg4fedNPTy8n7OdBjgKRl+5RcudUqy7G8ADDS9DkPnApQbYNjjsOlsz1/SzTeZ/V2bKiQiZk/HUvp572VWcTrfaNYVLddvTUkgyYl5Rk2c1NARtRbUkIHX3nrKyNNn/fgp6BCcL8E0DUxuHShNL3ULzBFqVCR1KwwlZi3LshnQbkDizMkS61i2dkmqDa2zMQpr7tuDc3FQyOmNH0TtD4hTZ97/1ckFLxDEniBCbpvUZMofzQjHdzp5/6oZaoWuQHLMLOrXmgQO34hvugzUMJgfMnvIH5I9rXfgRfTJpqv9P4vU63r7cAYPESmx2X+A7NgQLoCtn68q7BLNL3VK3U9DT1pp6lsPjzoBVj8mqFWSzOfW549B7ZwP4W67RB1fgm7ACh24T84DZTsKlm2/WKZOX499Z1l512OdYoPG5yqkho8qBAoIXMf+K5UNiyAch4axbdXwiWaVAU9BwZQqdHPJCAPD82/55aSHbp17BlY/JrXQLVmOoa/VTtnSqjqVuFI2LI3itkJmX6nCLrICVceIRVVD8iNTy2RK6yrtVGGypWshVke7Bg8kCqJzF2/Gk45LoGMynEf5HGPl6WEIl/yAIlPTjstcmWyP86tW+ZZoWvCo65sbvrblVLVcCuGCFtiUb6MCjQshH/oOquAtQJMbgKzp6TUD7lKVjy9Ua57bJAaaUZ2dnd0kb1KxblKXtAYPMxd/zss0VwibTB4sEGlaF7sl8AXZP61TjhxX1qqirYO2alTPx0c3JHetbGbdq6SmqZLpXUfAEUnAo7NWA8NYLNjkFBXQJj4NpxkTpMrJj6ixgXPTLNTXut6uLkoyTR4WD0zpg5kB7+3EbrpZ6g0veQyqh5abyTuRBSP1dsM8iYwnMBTn91ph4afdKw0jDrWtqForq+oh6KKmWyDoegOLO/MbuXOO6V20BekdS9ngRwyu+ftsaZ2DPIfmI8je7Rtvlwx9vuatdRDo/FJNe/3gzDZg8FD3RjH4KE/SzTsinTy0WOH9OFCWrl8hD4GPwZYSSj/wmzBqm2y7Ff23H7cZ856Lzxk6LfjLc30nFw69R1PKwmsrqGQL57iAg6DK3c+CCadoOIyhnoh8dzXy6EVhvvohMQ7bQyN18mNf1sHUURdSYdGCj6pnDd349H4RmDwUD0AULH5hQBVT+WaZwF0YEYkGA5KKGRZrfsWyS++fHGgtvofYEpArArjpLaXl93jZYd5N9SE4oTkM5vAd0yQln1k0vvLI/HLsZXa1TbCX7n1nNyw43z5zujHBZ7iVLhaYHfQqRYbafrcB8ZhDHkEk5BG6YD7oP63LVV04Q8AKhIxGuZjsAOVBZtx4C1JJKYmb/78Y/r8aKLKrsYR1A0NBAtfryxPcIkuPPbajjeUVTuOhBcUfM11E6Aa4iy4sqb9/4fPJVCvNrhADFd+QCqr/irLd8zSCYFjht3zjDRLvft0SakwdniYd/+ZuG+LhMKNKk03fj77VFiJMiuDjnGwuoG860a4ED1RAKojbttSwxrB7zYGHuc1MS55AF4IGUjLIUO46RksuNp/lYpqmC/Bw1x+mVln1mhB07Ju6AoMjf8iL738FTy3UzhrjIzm8/IfyKjTfdCc+6bC1eYa/Z7jMcfggS33BsPamDjTtfQ0c55eFtO95XiPzT0m5jUGU0Z6XhvyQ3g6ViXVtr2LZeWUiOZH2044YVL0dT3x5w8oFhys2rIeqiEwX2rPN6hMqx0n+G37YuDdpsqoY56V67eNV1DphCHSxeuZO/obU/7GMjn7m/PANyFNX6MC3XiCzvgpJjk4mDQTe3P0lMb0TNd4rzfde+wt1+TzXk8d66gWxUYC8P4f2yvtLWfJjQAV9e7JA7NtPg8haTzsPzD7GyUt72GbMqksYH35PWJoxDBbUTUK85ptsvypb8q3x96mz/TOSPtbCTV4wGvlJGTu+kWwSr7GFwYPfWoP51RoA4e+1n2PSqdMlZs/97aKSCKngOJGUvDrXiyTDQnsfqX4Z7bOCqc6DsHwNRcnYGjsgJQbHVE3+FZZ/vQdcC7u7CGDnar6XQUtI+L4aJpz/01gdK8BTwL5Gx9Egwd2vJ//teUY+kIBsCRBgGqpLPvMGXLzZxxQcVbrNCCti5wFBCfRL+2juMGW0QoseAzRd8D30O2fzUlP6+nc5DWxN583jUMSCm2Dw/y6QV+R007aKUu3nqRrkpxMcDjrS6BQ10jT59z/K6luvAyg4syPQ6wLqr4UWIq8Nih5NYe+Vuk48FmAap72Az8YB1Q9VwrdfNA3w9wm3RybOD2d5yakX2N6+nWTZtLNPeYc17lXC10yp93NcxM8uU1Sj7HJa2JvRm+aHjueelsh1qiqOREzx7/Jsh0X6mSC7gY5NOYSyKRz2xC24zv3rccQ8iXsRYOv2/ECnEsRpc2jQ19ch77Ols0ST5woy6as06GPn5f5YLJV0tu1Jp83zRybuKc83uvm2Bt7jzOVwTTmwT9dGu6ERS+TvDSV58UKFViH5EaNAfB6PwXftVofTD6J4oJsQaXpYGRnr6+V79y/GUs0ZykPR9/lbgN9G7NdNjeoDGLoqw2h3jfJDWefJiumvJbaszBX0VTf6Hu2Hs3bNQwT1m+kgiIRiB1KFSxYZFNLghoGdYMvkRuf3iHXbzlOxQUEV6ahcZorTZ/1wAjoiO3AyzkFwx+c8XORHKjiv5+DLRz6sA8h9nZuOzBdbvjMLK1uLkOfn9vl1g3A2r8CM8I9mEFV4qU4MiXvOzHH3th7zIK85+nH5rynfCYdW2ugnJBK/Ctrx0qw6hm5fvsXFFwcGimxN4HS9DUQfH573YfwPTwFg4fjpJM7PKjmhcnlV3Bx1ucOfe07xI6dJDees0Y4pNPFRy5DX1cLfXsUkCtOxxZjyU/Br9M+fEFUiekClwFFeszmpKeZc+8102xeM9dNbPJ5z538FbrkEghWQOZ1pyx7apUmczcqUi/VTQeoZq37H3g9T8J90FDpgMGDoVR8kLdMf1EuZ8ivqgthdWO1LPv0eFn22ZecoY+yKX+syjivYWC/kLxjFjYba3k37RwHbYQ/gU85qgDS977WMgyhZhL1SGJovFSWPf1ReIs7T2aOe1ULunTt2Vgp+L0EQcSiEF1YWDqiSoluDwKGg++HE0vDexhwmfO+1iYv+S0ubVHUksTQ9zVZfvYdWqzK71SU0L+nOFIv517vB9W/0gZ+l9vHkOSCtyIluHz0yxK1x0GV5GmdoeCVDfwpAyqBogJ82Zg1Vtd/RIKBF8B7nSUX/OJf4drz90qVYjHOBimecKgUY90ShOfuMaJU8B6nEgt+gFePpZlqLCBH25/DBzNaQUVAkXfMhwEK21Wath3ceW5d+PIkxSTPH7tXmk+eiOn6I1Dwo2ZDqcHF2nHxNY79jaskWLleRh33S2irQqVQfVIFHSDh3XHWrpQJLdPGeWMee/9ZbFECJkSY7VbVYcWh+eey7AmAaspz+iETUO4uDgOuSUkpcebad8mKuGCrZFlnh2fKsr/9FtP/86TNVfSDWkbmIoqQylljPEaib0lTExaGwra8/lpQosB9GE1QIOEqPxPW0tSUMa9pwEnqnIkmk3s5/1EUCo8V0E0TiBJmyvJzVusj2Mfs6/d5cCiWaSS/IkqxGWaffD70qWBoMIgyIcq4Uq9Irxfzx3myA5t43JKamoCMHClSXw+aSlkoMpihzwyFhKFJ1/vdPE5ZXdfy3Q5oTqFI6E5BRTvW8SK2lx+voNKPNgLWA338TxC6A4sNphRbjSkw/s8+eaa07fsurFhI2dhhfF2lDWTQoaggQVTpA0eJHHY4XiPOwetrDVNgwrnqiBtApRDl5DOtYP78BQ5vAlCFsSxzl7zcClHClB2iyoYAVARrmf8koWso9DZYFfHwdX0Y9oHTRy/CrOxt8AkrIZJALjpcpm51Xl+I9+m9H3MU05UQxEOHiVRXibwO7SRSrwoOjahbgJkQWE0e8oVztmji9Fkj0wcWMPRVVKiKTtuBy+XGsx0xiVKqyYXlVdlG/vshuN2YGVisoH5dnLWoOOImyJPeAc/wC3QcGGa4xnUMLErfFFaltk5k5CiRN94QaW4GuCBrVPGDCyRvLfkC2Hj+6zEO9Nh9MzzOOWhm3hhXteGOlldBTc/HssxmxzrpGbsYQ182omua21OT3FZrFzBPT+e85u0ak8+km3OVxOGkZ2DxDn7iEXQaxREzx/4SDD1M6m3sEAqO2ewQakrU/O6Pvije7p5785hrvGSu89jk8V7PlMdcN/eS0pih8UgMje++LfI2qoklOLXUZkt1wOcDkFfv1x8cI874Vkw+VgCB2RlMHXmsRejegwGIZ8JYjlon1rtfkhUXtOrQNx1C3CIFVstbtfTHZrtm8qbnST9nvkxp6enMw/+DeSzmTA86Y4Qa8eyT16NHT8UMrRVGCWTqM89uTA3MU7zlmWtMM9fT09Lzm3zmHhOn0vGWafJPkAw9TOQoAIygUb8eyJxi6HE9xXchnflT/ywUQctkunPaleY552HStZgJhi3M+uZBNgVVF4CKSzOR4oGKVTG457FfQm7AYm2pm04d9dknPwFqMB4znjexvkjtg6J9mVk7jUBiIPWqw2zxmFGC2SNqh+oRPBnB5QJIQcRjPXDKMYBzzry/zARVH7Q9EXsL1uEfx1rfUkdZ8dBQG/Y2plDHuQOLNTDgumL03/Gmxkln6y61fPYLuFhHMzSGgfkjj3aYezL1pGgMBjA8V+rlUjoDKr3uZE3l7zoFakHLqDbc0bIRVk0nyopzHtO1Pi4eF8ukras+vj3qG7DYDIKLPNd3xr0lweYJ4C0ehx2iX6T0TkcTXAZIFEccBYBxFqgyL2QheMyQmKJkSNc0FpGJenHoCwWx3hfAWl8EVOosWXnuPh36etPwZJH/ZKEX5r2H3lCeC7PFKyzunv1Ruf6pP0BK/xnIvEAaqFvFt1jqgCpw0OLQWN8gMhLKjG+8JtIK/1ecNbKGJGLpNeU9BCbTnWOagsZgMQOFRGiAWPHpECVsVIuZnR+Gh8PpmflM3F6UwDawnvz3S0Bd+k6xTOW5eG10pOaMPRuLxXCuTyl90hkuTL6Sx0AIlxXDIKpHjRQZMsShXPTa56VcqSHQTdcXBbJnQ2BWSWPR1kelDUPfcoBKVXci9vtFd6oQr6j/wGJtqCNFjUeGOWMuxBLQcixekwriO/KRLwHv0Hj4CPBeRzqgov0qAaT8Fqps+C4m2kkYY0BmEa4KghIvlZXnnCG3nveWZ+hT6LHp5XBwD/RvKPSWo0wrZkMMc6zZGBbfgT+t66DRyY6nkagDPM1Qwh+Ci4FDYwMWsisprcfQ2IGhkYw+KRazONloLIq1vvZWiFa+BJP2dchgybQ1MBYt8dCnjfD/z8AolmkfZ0NXA0JcvpgDt0Wdzd/Al45XFODSD4dG/wQCjOCiAcnRI0WaBrtMPb8D12KmAgvIHa2bpS3+IQUVhz6G94nasLalwD8Dp1imgo5ukSulH/NjuQGUywrdA78JQch7yGLmB8TmeQONCS6CbPgRWDSGu5Z/vAF+CkNfTS1mfXtXya1TL9dHqCXQIaLmwm/DBFJe7znT09PMuYl7upfXGdLLc1K7/7p58/+yOWNUccTYtVhS+QgsaPaBcPE5BJd/gndoHDTYlpHHBeB36oC89drZCirObA8VixkltmldmwkE6Wnm3MSmiEzn6Wkmb3rMfPjPP7D4oK3u0/78Z5FX9jhMsVq551o79/5iRDo0gomvAs818liR8ad1PXVa12H5qA89AKqVf2BxrYwCw5lrPieHD31C2jub5OUXk2CCAw7L5UNwcUxMQKHLCjTAKvsPct2Om5Bkq24aqW859LkH8ggsDB1k3uli59/v/jqMYO9Rvj2AmWFHR0BefglMMhza0LKGMzD/hQDUgRyj2fpBl8l12zdL5K9HpuwBVHTvv0r7tUb5AZb6bVoMYEFL8lv3zINu0o/AsBM9CTCMWAbBHCEOATXB1Y7pvU4WcZk5/PXfZTRLy6Cq6ufke09+VsHFehq1bb++TR/Va+CzQjK4EXe/5MvWLoerSeyXDPdBAVCwpOvFlxQqRHBhJrZnt7M4TOU8MzPzUYe4VaG4gS6FatGetfK97ddjaJyLa47RrB+NIQh8PwTOClGXgVEsgsrIdi5f+zOs+l8Bwahj7UuzJz5E//mDgDVcPX9lt8iB/Y7euj+HRdbWMZqNtiakfvAc8F2PSmTHYV1Do7aE+UobONf2C6jYE6gL33b/gUV+KgWqdX+AT6oLoOnARWhnhwcDKM669JhPRSCPxf/XXhHZS01PHPuqZ7SW5of9E3CNZj8mVbJLrtl+loIr4oojTM4Sxn7CFbuB9enfUKhakpNj8h93VUuyGvsl130UVilw323R94MTCCg9YYygp7hI1RQaOpB6vQEDCA6H1PrkOp03v97kgx/HnpL6V1CbqWgCv7hevrttsSy0Ilq7EgtQ2Wump33QW6kq9B1YpiMvvW8YhJ+PqdO0Ds9+yYoj/Ojam56kHqYHmgRwMaZFzVtvOuAaNtzBlR/B5bSAGqPYGAFbi9QPuVq+t+NfJJqcJpEJ+4QuLrlDWjmkeqBvQ6HZ4eGydcdLAO6DKqpPhI4SVZOp6OeARYHhAsekKZjwo8OiJ+Z16kbRAOLNV3nm5NElO5RhyvJLTHea3MiJ/iRq6v+3VFjPy7VPflxBxRmj2mNqK/7pf3IHltnh4fJ7P4IZ3zZoUg6Hdxp+pQ6oTFfqEOieKKBwzDh17AKLwyH/mR/meLJ/L7QN9jhDIvdB9CtTr0qMNneIiKEPhmH7lEfku9vnOoa+WIw3C9amP4oR+/AbzA1Y7Cxanlz++08CCI9DRbcWPgnIqGeWSnvBpR0L8JiYh/qPH+bTf6RREa8FNoFcAqKtoJ/B5bQFewZhO7141IaD3u9DJLFOZu+o1VUHP26np3Uu3k8vwDLSdCzRzFr7ZciiHgAQsPwBuyobfEW2LyVFokxjCCIee8BkQMV02v8RXNSPenk3zOYxwhpBKm/LFkw9TB6eDySY+03MsjIeu3I6qmTXYM+gwfYuWbLlNLULoHZtUYdGVtD8pzc+vfLefOnXvPem5zN5M6V3v9YzsChNp2Ibpemz7p0l4dpfqOeUJFSPnQ0wvTXo4ViR5FxLgYqnBlxpMTuGSnc02SK4OqBST3AZIwfTnvTYeUL3fk3P05dzlsf8DOa+no45a6QzXe4ZFKr6APjOv8o12y4v6p5BWlHvj6m0idMrb/Jma6S5Zu5Nvyc9vft5ZmDpEk3E0em+7N5rIX1eIdFWzGyVq6bgKfdAqmSCF1wpapUJXHgEh8NXXoILoBa/C1JN6+CyqBOUPI7t9AatxKzxLrn0vkp1bPdPODQeDCxdook4QqVZa2/D7GchZDjcL5mwQH7zFfQh5p0KKjc2YPOCy0vF+Koo5+KzXtkNfwz7HHBpAp/r2wALJdSNGyPUNE6DEuGzsuTJcY49JgTKzpYsvq18PivWHVjdpOlrfwvd9RmuM/787Jes4HIB6QUaW6Tgw4+Czb1IqTyZ+NdeFtn3btKR0pMZ85GhxsFvg5V39wyqHgWB6na5ZutMZSnoaIV9XKrQn2+yP/egfV2NNNJ0xjXDN0ql2S8Z0vR8BgLHK0pgxV0cOY/xnDAflU8tGIu+9aYzWRgCt0UJOF9znMH1bVjOZzt6L8vZM4h9zD2Drt3+LxIf97WUz9d8LWSbBYtcAZBrPm/7+noP8jsUS5XzoEc16+4mqR6+RSrqzkjtl8xCc/lnRUw+7zHTTEgde8Bz0LCIzLys/5oPFjPwkxCQDnn+2bPgYO1fwSTjOrh6G4Ya5pn+jDE0grpS5lXb9FUJP7VTIls+5Kw1gnL5wrDXvJz8xpj1oYFUzrt83dGSCMAZf81Y8FQHO+Pv7bkp0CCj95j3mZdujhl7yZSCCyDSYZCXcBzACwnAKLYGxqLxts2QPxwvd39zo8w/+ddQGDxHqR73Rwa3rMX59kc3RjBD44cw690JvusCHRqppVrKobGAfeaIEy5feyImfNg2pOooSNO7lmgK+GAt2lArnihxYkyAYeijAUYl9pjpaF4lt00/TX70lVfVWJSqwgvH/AE5T8eQ2AEzMw7n7jpdOqJxJWPIlC/XtPQCM92XMQ/ccbt7BtU1/QwiidWai+Kc96H6c0C+xh1W7fWQvzTBNVHxQGX6vhu4lGphjxnsLCrJKDZvmp4yw1KLGVBW9RsBd0oLxv4FIJyAOr+NZRXkx7ZsGviie/tnxvQ8uablcl9PecBv2VDj0D2DBl0i127bLpEnjtU26VIQBNLvkxCQ+v1XQJfqGHxNUEjHGthBHZ7eSQU4d7qTbChcLtLbMPaYSdgnyQ/Py7zHjHGntGDscxhaMHS3/re6asy0XYvBi/eFsQnpwdsscy0930DymHsdvha7fWEhu7J2HDZAfxYAm+bsSYih0fjDMHXoT2yeZervPc+WxmeZ6+bYe6+pS7Y0Nw+1PKfiq+dp1wzRFFC8OA5+Co72MfS1H1gtt5w7Xn54/otZ95gx7pTmjH5T4vvGw2nHFvVbZcDFupsO8B6bjjPXTOxtqzfNHDP2BpPONHOcLY+5tysvvdeg3dgzqKr+Llmy/SbNYvYMMvlzibvK7A4M3ptep2xpJq8pL/3ZmdLT09xzzgpH61INfSyYTMWMsYCDWV4Iin5JqDVfAFDN1PbopAJrlNmCDos2BI+TW2Th2FMByg3ujhrZ78tWZlGvweWTs2cQttNrugx81+MQS3xAh0blu3IbGv04ftIqJTOqC93BxtF+pTrafw6mYqNl1bl36CyJ03AytbkE406JM6xF4z8Jge6vAK6wTgDcvWNzKaaEefhxO0NjVd2pON4li7dOUXDx3ZC37CUwmwnmdZrYpDP2ppljE5t86efp96XnY34TzDFjSh//Bv/kvAYd4aIFGFzgj3vMdGCPmSHbR8stU59z/E4BUI4fiNwr43WntHDcl8G/rJJqgIu+rfwtpfe2EbNG1zKouv5eiWxbqv1Au4JeZo0GDObFegvNdC1bPnNvT/eZdJOPsTfNlM09oddgys7ruVEI5hxIsDj0VYKfAwFvb54Jby4XYg3NWeoYiMtFvgCqqZDaLRp/OcB1JZakKOci3YLDD1Ta7/9JWgbBRq6TlkGD5sjiJx+V2X/2n2VQDu8/IC17V2L42A1xA/z64KUXKujQR5eL2F4tFn0RBHI8+KnVOvSpNkWOQ1+2+tGdEgP5s0Xjr8WOW/8GgS9oozqOKCZFzlbL7NccXjfoWAY1fEzqq3fJVds+4YhZImARXF9k3lL89MGwXqhPQH56UQcsts6CuOE93ahR5UF5rimXXshhco+Z9pY1MrTzJLn5vB3gHypSi7PejhrIsUqzAVLKha4afytEF1NVpyug6hLFocoDqT/vdWR7mDU2O5ZBVdUb5KqtEYey4+Pxqj87n9JAn5i/+wkdBGdJZ9WUFyCQxA6rHS/hCyfDxSWd/AwdOuurgHYEFNw7sMfMLZ+frmrOpCrc17lQgcMqeZNF434LmdhkDDHc8zAEgB8iM0b2P3eOhWVQtD0J58FXy5VPbpBZ25oo83r++TfyqxyQ5/fgLOlwEfrm818Vq3U8KNc2LEI74BrYwwBNDH0sK9b5GmZ9p4FKrdJZTr6Gvt7qZ6T0V47ZBHP5SRCr7JMwpfQ+B5f71euXzaGRE522/di0oP4TUpvcJXM3f/z1mUdAhxvB5jDvv+BUiovQpCA3f+WADN1xCuRJD6oEnIx2/4KjdVCFPWY6WtZJdcuJcvO5m3XoUyZbFQn7V3Jf7zJS+oVjnsIEcTx2Z31FKqAtQUGqUmXi32f/rJjWCY1lHeFgCb/caTYGqnsY+OFHAnO3LGJXBMIWWBkeaU498MOPUyVTE4LLyI++dc+vAa4vYvji0EGpfPe85p70mGAM0CoCIR6dJ/953lI99patCUX+4bBICjbvsUFwB/kIpPRj8KL44fhzSCGgGBh3Axn41aQdsGqHWPZbr99+zLgT3gsNGvrtROuBBF5QrzIvLbPQP6jzwWDxOvr41t2rAK5LIWvijIp5s5FdKi9j6KvlUsVbYECnQpTwmA59o6fZUHArPZtJptfwXv8IboT68Bk6xPgRXClg4cALLvZiEjIVKDraiXBo2PB6qR9xmG1z59kMr7PQGMpYPup7MFCcocqRB91y7mUA1VVYLOVyDypuPCBrS1EmYwam47OiFL2zZSPoG4Y+BVWFOg7xA6hYTYKKlJOU66oJZ0rr/rtTS0CHgpReuxs/jmVQCPywI0JxztlC34SDKZapGgWN02H+RaB963ffxHreDzGzwteC3bNtBSTvZZOgkIehjzxkLBqRH5y7WIswWqmmPD/FNIfnFsUMS7bDYKRxBgSqFEVwKOm5T5i/WKELROxl/W7R06RW+g+KJXZnTIYdNVzqh9ONBprjVUEqVj0zPQd1660TIZB7GIaXkAv9+2+mADy3Y2gcgoVTp6E0dCCgOg5g1pe8WH4wbQNkLQHhHjMEpJ8DBY0RvjK8osg27HvduACCYjNcH0zJi90W1Ayd7DyVkbGtzAiswwAsfBeHELCchhnqM+OuRqkIXoD2noEL8LxvvY3GPCTh2B2yYnq7sx3ITH75bo84t/v210uVl2ybBf5wBfhDNAvrpqXeUUN70O1Gwt0w8OnAOhIUa8ShCiwio7dZXW/X/YsuUOVNDlVevA1uBCp+4Qz5HPd1NlyamucCrA53KDykgcXu5Rc+c3VI9g5KymhspE3m//UR2FptBumw+3mV5j0M+KlGHBHZ+ikM7/dj32sMP7EYKBcFqsUP2YCFncu681igWLQcTx8KWUZPzE62awNtLcru6bEDLfrQvJ+m8CpQ3X4q3tzD0PqogXYt5XjFB1cmYPXIvPtvKCw9k+onCBopfWT8ZkjksLzV+aYadqSk9KgsX3gx/k2/pD/LpPs8LgMr/QUZXfprJ2Bh3sK+123P6/JWIVWK0utgzg2ozPkhNL6UgWVemjemAJU8VwT7XjcMHQ8h8eNqqFEIlaKeyB9ngb0FD/ByyN1baXm9fgh9A3ltd26FRWysnVqcHWJW/OR9kNJ/GoLU4vBcBilGzGBil8/iAhkFpEMpIPXhrLBMsbJBzBhqKLAmcjN17HsNXXpHjdu8+mwl9P9apk9eKRR+0p9szvU6HlnK2G1xGVi9vXqvoUZk4oUA140w8KW2B2hGARfWDThYP++x1tckmNjNo9dK/ONWKdN3UeKa+fTxRtecC+pXbZkPcH0PqsPU5GdX5v8DZakMZgg0cZrkXYfC4Yf7bkkn/x3idMf775eAigBEXGFYcgo3U79EQtXQ7cSCKXX6CxIMunoo3KUOPVwtaXIZWH3qfi5Yu4YakQk/go3A51XafUi4U+pTQweWGSpIZWD1pwtVrwtS+iUT1ko8/nFVJaKtZKENNQyFMnF/6l7Ye2wrCIc62GukDKz+drSR0l8z6TG4s5yIPXbew9BIXXqKI7oYbgOCvsbeepl7vWneY/JfDMqH4bhEMRj2hAXjZxCsp8vAcl5J/34NuK4av1OsTpjPte+GQa5jqNG/ErvuUjDhVbmY6bqQ6cgFU6ZLRUyzEdRnbFIeKANroB1vloAiH31VAlEsAbVsd6T0/bZw8tQoJ1Qhvy8m99ysPZxs2d8mVujnZWB5XmO/D7kEREONyEcPyMkTTsEuFX+ERir2boRdZSGDUrVCPqAPZYMFCNbX84Yluy+qe7MMrD70XdasxlCDAtVrJv4vWP+sgadkDIsElzI9uN0gIdfYPNGlXOnFmMsljdFeLNAHmxorE/v33bv7oqalaK4/rWhL2k8DeThFEcaf1ZKJ0+HY4wegXOS5YAuI31zx5OJI8cib9L5UImpojgdS2QHfS0DFrFA4GKxrrEjs3/9fuy8e9DktdbFaQgz4AeUCvD2g5nOuO6VrJv0fDItL4E4JogjVsOUScm7BYMfEB93FC+b/oIuFTEiAcMYx+wsGahvDyVjnq4nmfReDUn1RH6pGKlayPBQW4hUYO0qamV0z8WpYMV0KhUH0tY4Q+ZfSG/AZnBUmTuDTiFsVNcFgbWPIjna8YLfun1H38uvHgVLdnvJN5rbdF9OJQrxbn5RJf1Yw1IDqzZVPfhEbiP5aN1BLUkG9F0MNBQt+UiBxj71rhdCaHno07QoLt1YIgGA7M9BbGC1zM5Bk+4GnwUIt3XNxw69cKixnPmyHNk121Yvcji9TrMIiEEtA6HAqDV4z8U5I6T+hzmyD3GW9Hx5vCDIGAzZzrIn5/eFwhxITVlVdKFBVF7Q7W59IdLSev/viprF7vt74S4JqEtsFlKWDijUpU6z8vo+eS0sZamyBlN7aBEONejXUsGCoYQDjvVvT8GNAZGaEXoqVUvTLH8VS/snCnmvVDQFa/tjR1sdsO3AdwHS/Wz0LgAptnTmJZkGZaq7ZysDyvsxCHxtwLdoxCiPMn2AgewQc2mb2eJMJWAQV0xHzlXZpkA4YWCiNQ5kdCtQ0WuCf4NmscwOmsd/f8/VBD2u3gDKduUmCmahTpm4rAytTrxQyLWW/uGUoKNejMNQ4CTr13cFF8GggenDgpVY89wLr6BHgsQ7DOjidTff5dbI0DnlhAEqSHdjNNh5fiw3Llu75+pC/IB0q2aqoAHcE3XkovZblp881yVJW+VKuPWDcKV36QqU0qJT+dPiN6AIXX7cGHPA4G7COGiF1AFaffDfQsw55KMsCoBok2bqfT7sTz7l+zzcGbdNHc+uVZzZh8jGZwOtzKAOrz12Wpxu8LgkWbr0XUvopEKimGWp4gEUJmAGZl2L1DVgsAYAKhAO19ZJs3o9j+Zkkg8v3XFL/rLaM9frwmbZulj6AppaBNYDOG/CtpApcAmJYuPUnMNS4SNr2gmO2HP/0TDdgMjEBRmDxkjLvOVEs3pWA92hQqDpJtuxrR7k/hl/AFS9f3PQirolAZCBvI9XURxP7/1MGVv/7Lj93upJqLWzh1qWQ0s/pcqdECT6umKGQxwAV05R5j9L8C8A6vMehUAFlBcNhq7oGgNp/wLKTt8bDVatevbDmNT6TIoOtg15MpvyFMTEPoQysPHTigIugTzG5Gowy9OoXPPEdMPTXwykwAUVqxi2KFUwKMgUWKRYYpWhchn/wGGCxAU5BkLWLeQegrATW8cJWVTUB9Q6YqlsqrI4f/P3iEW+zvpNus8Nb3wAVM6sEA25E9wLKwOreHyU8gycf405pwdavwZ3S7ZIAP5+EOyUbUvoUuFxQxeLY76FSjjj+WC+giK6kFa4IWxVVYMr3vQ5ErgzG965+ceZxyqErhXpjUsEAZTqwDCzTE36JzYxx/tZzYFS2DkpzdMHZiRcF7866L5DjFhJE7IhRx8CNfi135COg7EBFdQig4izvJSwdLU80t/7k1SuOamfTCk2h0ruvDKz0HvHDuRGkLtx2OpistXZlw1C7+T2w3xBWwV9yuKpKhh0xApuIYJ+gZDJgVdUGaMQAQD0HfN2w59Wmnxu5kwOoxaBQEfJbRQtlYBWtq/v4IJdyjbx9b1Pzu3uvxXB4QbCmqT4MnqmiBgYL4Mq4aZ7diQleLPokhKM37P5G453mKTrkzcCyi6OuY5KLFpeBVbSu7seDPLKuCffZw5rfjU6OJ2LjIQyFm2S7E2B7MRBI/OmlbwzdbEpXQPWyjmfyFjL+/4JPu45FLkyEAAAAAElFTkSuQmCC'); background-size: 24px; background-repeat: no-repeat; - background-position: left 50%; + background-position: left center; padding-left: 36px; font-size: 20px; letter-spacing: -0.04rem; @@ -42,7 +42,7 @@ body { .message { font-weight: 300; - font-size: 1.3rem; + font-size: 1.4rem; } body.error .message { diff --git a/extensions/vscode-account/package.json b/extensions/vscode-account/package.json index c4df52ce732..cd579aef85a 100644 --- a/extensions/vscode-account/package.json +++ b/extensions/vscode-account/package.json @@ -39,5 +39,8 @@ "tslint": "^5.12.1", "@types/node": "^10.12.21", "@types/keytar": "^4.0.1" + }, + "dependencies": { + "vscode-nls": "^4.1.1" } } diff --git a/extensions/vscode-account/package.nls.json b/extensions/vscode-account/package.nls.json index 8211a3f6e9c..c0bb4c4a6a0 100644 --- a/extensions/vscode-account/package.nls.json +++ b/extensions/vscode-account/package.nls.json @@ -1,6 +1,6 @@ { "displayName": "Microsoft Account", "description": "Microsoft authentication provider", - "signIn": "Sign in", - "signOut": "Sign out" + "signIn": "Sign In", + "signOut": "Sign Out" } diff --git a/extensions/vscode-account/src/AADHelper.ts b/extensions/vscode-account/src/AADHelper.ts index 94bc3695dc4..5f193828141 100644 --- a/extensions/vscode-account/src/AADHelper.ts +++ b/extensions/vscode-account/src/AADHelper.ts @@ -295,8 +295,8 @@ export class AzureActiveDirectoryService { private getCallbackEnvironment(callbackUri: vscode.Uri): string { switch (callbackUri.authority) { - case 'online.visualstudio.com,': - return 'vso'; + case 'online.visualstudio.com': + return 'vso,'; case 'online-ppe.core.vsengsaas.visualstudio.com': return 'vsoppe,'; case 'online.dev.core.vsengsaas.visualstudio.com': @@ -397,7 +397,7 @@ export class AzureActiveDirectoryService { const claims = this.getTokenClaims(json.access_token); return { expiresIn: json.expires_in, - expiresAt: Date.now() + json.expires_in * 1000, + expiresAt: json.expires_in ? Date.now() + json.expires_in * 1000 : undefined, accessToken: json.access_token, refreshToken: json.refresh_token, scope, diff --git a/extensions/vscode-account/src/extension.ts b/extensions/vscode-account/src/extension.ts index 88f5f3133ed..fa94280fe3d 100644 --- a/extensions/vscode-account/src/extension.ts +++ b/extensions/vscode-account/src/extension.ts @@ -5,6 +5,9 @@ import * as vscode from 'vscode'; import { AzureActiveDirectoryService, onDidChangeSessions } from './AADHelper'; +import * as nls from 'vscode-nls'; + +const localize = nls.loadMessageBundle(); export const DEFAULT_SCOPES = 'https://management.core.windows.net/.default offline_access'; @@ -45,6 +48,7 @@ export async function activate(context: vscode.ExtensionContext) { if (sessions.length === 1) { await loginService.logout(loginService.sessions[0].id); onDidChangeSessions.fire(); + vscode.window.showInformationMessage(localize('signedOut', "Successfully signed out.")); return; } @@ -58,6 +62,7 @@ export async function activate(context: vscode.ExtensionContext) { if (selectedSession) { await loginService.logout(selectedSession.id); onDidChangeSessions.fire(); + vscode.window.showInformationMessage(localize('signedOut', "Successfully signed out.")); return; } })); diff --git a/extensions/vscode-account/yarn.lock b/extensions/vscode-account/yarn.lock index 3acdda242e9..4a86ea6a2a2 100644 --- a/extensions/vscode-account/yarn.lock +++ b/extensions/vscode-account/yarn.lock @@ -635,6 +635,11 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= +vscode-nls@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.1.tgz#f9916b64e4947b20322defb1e676a495861f133c" + integrity sha512-4R+2UoUUU/LdnMnFjePxfLqNhBS8lrAFyX7pjb2ud/lqDkrUavFUTcG7wR0HBZFakae0Q6KLBFjMS6W93F403A== + which-pm-runs@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" diff --git a/extensions/vscode-colorize-tests/package.json b/extensions/vscode-colorize-tests/package.json index da71634c2e7..d99e050d302 100644 --- a/extensions/vscode-colorize-tests/package.json +++ b/extensions/vscode-colorize-tests/package.json @@ -17,7 +17,7 @@ "vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:vscode-colorize-tests ./tsconfig.json" }, "dependencies": { - "jsonc-parser": "2.2.0" + "jsonc-parser": "2.2.1" }, "devDependencies": { "@types/node": "^12.11.7", diff --git a/extensions/vscode-colorize-tests/yarn.lock b/extensions/vscode-colorize-tests/yarn.lock index c6b3fdb4313..b8a66d65eff 100644 --- a/extensions/vscode-colorize-tests/yarn.lock +++ b/extensions/vscode-colorize-tests/yarn.lock @@ -1042,10 +1042,10 @@ json3@3.3.2: resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" integrity sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE= -jsonc-parser@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.2.0.tgz#f206f87f9d49d644b7502052c04e82dd6392e9ef" - integrity sha512-4fLQxW1j/5fWj6p78vAlAafoCKtuBm6ghv+Ij5W2DrDx0qE+ZdEl2c6Ko1mgJNF5ftX1iEWQQ4Ap7+3GlhjkOA== +jsonc-parser@2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.2.1.tgz#db73cd59d78cce28723199466b2a03d1be1df2bc" + integrity sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w== jsonify@~0.0.0: version "0.0.0" diff --git a/package.json b/package.json index f1a86363326..09843bf7d28 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.43.0", - "distro": "ce5d74deb336afe4dcce871e6c5b221aed44fb69", + "distro": "e16fca95fbe6abb7e846db3fd372c95da67a41ad", "author": { "name": "Microsoft Corporation" }, @@ -93,6 +93,7 @@ "copy-webpack-plugin": "^4.5.2", "coveralls": "^2.11.11", "cson-parser": "^1.3.3", + "css-loader": "^3.2.0", "debounce": "^1.0.0", "electron": "7.1.11", "eslint": "6.8.0", @@ -100,6 +101,7 @@ "event-stream": "3.3.4", "fancy-log": "^1.3.3", "fast-plist": "0.1.2", + "file-loader": "^4.2.0", "glob": "^5.0.13", "gulp": "^4.0.0", "gulp-atom-electron": "^1.22.0", @@ -146,6 +148,7 @@ "rimraf": "^2.2.8", "sinon": "^1.17.2", "source-map": "^0.4.4", + "style-loader": "^1.0.0", "ts-loader": "^4.4.2", "typescript": "3.8.2", "typescript-formatter": "7.1.0", @@ -153,7 +156,7 @@ "vinyl": "^2.0.0", "vinyl-fs": "^3.0.0", "vsce": "1.48.0", - "vscode-debugprotocol": "1.37.0", + "vscode-debugprotocol": "1.39.0", "vscode-nls-dev": "^3.3.1", "webpack": "^4.16.5", "webpack-cli": "^3.3.8", diff --git a/src/main.js b/src/main.js index f8aa90f9c8c..bc268c8cd12 100644 --- a/src/main.js +++ b/src/main.js @@ -136,7 +136,7 @@ function configureCommandlineSwitchesSync(cliArgs) { // override for the color profile to use 'force-color-profile' ]; - + if (process.platform === 'linux') { SUPPORTED_ELECTRON_SWITCHES.push('force-renderer-accessibility'); } @@ -344,7 +344,7 @@ function setCurrentWorkingDirectory() { function registerListeners() { /** - * Mac: when someone drops a file to the not-yet running VSCode, the open-file event fires even before + * macOS: when someone drops a file to the not-yet running VSCode, the open-file event fires even before * the app-ready event. We listen very early for open-file and remember this upon startup as path to open. * * @type {string[]} @@ -356,7 +356,7 @@ function registerListeners() { }); /** - * React to open-url requests. + * macOS: react to open-url requests. * * @type {string[]} */ diff --git a/src/vs/base/browser/touch.ts b/src/vs/base/browser/touch.ts index 9956bc9112b..f8b32d9cb76 100644 --- a/src/vs/base/browser/touch.ts +++ b/src/vs/base/browser/touch.ts @@ -90,9 +90,9 @@ export class Gesture extends Disposable { this.targets = []; this.ignoreTargets = []; this._lastSetTapCountTime = 0; - this._register(DomUtils.addDisposableListener(document, 'touchstart', (e: TouchEvent) => this.onTouchStart(e))); + this._register(DomUtils.addDisposableListener(document, 'touchstart', (e: TouchEvent) => this.onTouchStart(e), { passive: false })); this._register(DomUtils.addDisposableListener(document, 'touchend', (e: TouchEvent) => this.onTouchEnd(e))); - this._register(DomUtils.addDisposableListener(document, 'touchmove', (e: TouchEvent) => this.onTouchMove(e))); + this._register(DomUtils.addDisposableListener(document, 'touchmove', (e: TouchEvent) => this.onTouchMove(e), { passive: false })); } public static addTarget(element: HTMLElement): IDisposable { diff --git a/src/vs/base/browser/ui/dialog/dialog.css b/src/vs/base/browser/ui/dialog/dialog.css index 1249a329acf..876f2168fe3 100644 --- a/src/vs/base/browser/ui/dialog/dialog.css +++ b/src/vs/base/browser/ui/dialog/dialog.css @@ -93,7 +93,6 @@ .monaco-dialog-box .dialog-message-row .dialog-message-container .dialog-message-detail { line-height: 22px; flex: 1; /* let the message always grow */ - opacity: .9; } .monaco-dialog-box .dialog-message-row .dialog-message-container .dialog-message a:focus { diff --git a/src/vs/base/browser/ui/dialog/dialog.ts b/src/vs/base/browser/ui/dialog/dialog.ts index 46190302d98..97bcd36f292 100644 --- a/src/vs/base/browser/ui/dialog/dialog.ts +++ b/src/vs/base/browser/ui/dialog/dialog.ts @@ -267,7 +267,13 @@ export class Dialog extends Disposable { if (this.checkbox) { this.checkbox.style(style); } + + if (this.messageDetailElement) { + const messageDetailColor = Color.fromHex(fgColor).transparent(.9); + this.messageDetailElement.style.color = messageDetailColor.makeOpaque(Color.fromHex(bgColor)).toString(); + } } + } } diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index ade0ed5b030..720ea3a7a9f 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -588,7 +588,7 @@ export class ListView implements ISpliceable, IDisposable { if (!item.row) { item.row = this.cache.alloc(item.templateId); - const role = this.ariaProvider.getRole ? this.ariaProvider.getRole(item.element) : 'treeitem'; + const role = this.ariaProvider.getRole ? this.ariaProvider.getRole(item.element) : 'listitem'; item.row!.domNode!.setAttribute('role', role); const checked = this.ariaProvider.isChecked ? this.ariaProvider.isChecked(item.element) : undefined; if (typeof checked !== 'undefined') { diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 9e584efcc56..fc78cfe1c28 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -196,7 +196,7 @@ function asListOptions(modelProvider: () => ITreeModel { return options.ariaProvider!.getRole!(node.element); - } : undefined + } : () => 'treeitem' } }; } diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index 6ddb3fe5cb8..0cd13de8079 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -267,7 +267,7 @@ function asObjectTreeOptions(options?: IAsyncDataTreeOpt }, getRole: options.ariaProvider!.getRole ? (el) => { return options.ariaProvider!.getRole!(el.element as T); - } : undefined, + } : () => 'treeitem', isChecked: options.ariaProvider!.isChecked ? (e) => { return options.ariaProvider?.isChecked!(e.element as T); } : undefined diff --git a/src/vs/base/common/date.ts b/src/vs/base/common/date.ts index ec970c13103..4655bc663ee 100644 --- a/src/vs/base/common/date.ts +++ b/src/vs/base/common/date.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { pad } from './strings'; +import { localize } from 'vs/nls'; const minute = 60; const hour = minute * 60; @@ -12,7 +13,6 @@ const week = day * 7; const month = day * 30; const year = day * 365; -// TODO[ECA]: Localize strings export function fromNow(date: number | Date, appendAgoLabel?: boolean): string { if (typeof date !== 'number') { date = date.getTime(); @@ -20,36 +20,99 @@ export function fromNow(date: number | Date, appendAgoLabel?: boolean): string { const seconds = Math.round((new Date().getTime() - date) / 1000); if (seconds < 30) { - return 'now'; + return localize('date.fromNow.now', 'now'); } let value: number; - let unit: string; if (seconds < minute) { value = seconds; - unit = 'sec'; - } else if (seconds < hour) { - value = Math.floor(seconds / minute); - unit = 'min'; - } else if (seconds < day) { - value = Math.floor(seconds / hour); - unit = 'hr'; - } else if (seconds < week) { - value = Math.floor(seconds / day); - unit = 'day'; - } else if (seconds < month) { - value = Math.floor(seconds / week); - unit = 'wk'; - } else if (seconds < year) { - value = Math.floor(seconds / month); - unit = 'mo'; - } else { - value = Math.floor(seconds / year); - unit = 'yr'; + + if (appendAgoLabel) { + return value === 1 + ? localize('date.fromNow.seconds.singular.ago', '{0} sec ago', value) + : localize('date.fromNow.seconds.plural.ago', '{0} secs ago', value); + } else { + return value === 1 + ? localize('date.fromNow.seconds.singular', '{0} sec', value) + : localize('date.fromNow.seconds.plural', '{0} secs', value); + } } - return `${value} ${unit}${value === 1 ? '' : 's'}${appendAgoLabel ? ' ago' : ''}`; + if (seconds < hour) { + value = Math.floor(seconds / minute); + if (appendAgoLabel) { + return value === 1 + ? localize('date.fromNow.minutes.singular.ago', '{0} min ago', value) + : localize('date.fromNow.minutes.plural.ago', '{0} mins ago', value); + } else { + return value === 1 + ? localize('date.fromNow.minutes.singular', '{0} min', value) + : localize('date.fromNow.minutes.plural', '{0} mins', value); + } + } + if (seconds < day) { + value = Math.floor(seconds / hour); + if (appendAgoLabel) { + return value === 1 + ? localize('date.fromNow.hours.singular.ago', '{0} hr ago', value) + : localize('date.fromNow.hours.plural.ago', '{0} hrs ago', value); + } else { + return value === 1 + ? localize('date.fromNow.hours.singular', '{0} hr', value) + : localize('date.fromNow.hours.plural', '{0} hrs', value); + } + } + + if (seconds < week) { + value = Math.floor(seconds / day); + if (appendAgoLabel) { + return value === 1 + ? localize('date.fromNow.days.singular.ago', '{0} day ago', value) + : localize('date.fromNow.days.plural.ago', '{0} days ago', value); + } else { + return value === 1 + ? localize('date.fromNow.days.singular', '{0} day', value) + : localize('date.fromNow.days.plural', '{0} days', value); + } + } + + if (seconds < month) { + value = Math.floor(seconds / week); + if (appendAgoLabel) { + return value === 1 + ? localize('date.fromNow.weeks.singular.ago', '{0} wk ago', value) + : localize('date.fromNow.weeks.plural.ago', '{0} wks ago', value); + } else { + return value === 1 + ? localize('date.fromNow.weeks.singular', '{0} wk', value) + : localize('date.fromNow.weeks.plural', '{0} wks', value); + } + } + + if (seconds < year) { + value = Math.floor(seconds / month); + if (appendAgoLabel) { + return value === 1 + ? localize('date.fromNow.months.singular.ago', '{0} mo ago', value) + : localize('date.fromNow.months.plural.ago', '{0} mos ago', value); + } else { + return value === 1 + ? localize('date.fromNow.months.singular', '{0} mo', value) + : localize('date.fromNow.months.plural', '{0} mos', value); + } + } + + value = Math.floor(seconds / year); + if (appendAgoLabel) { + return value === 1 + ? localize('date.fromNow.years.singular.ago', '{0} yr ago', value) + : localize('date.fromNow.years.plural.ago', '{0} yrs ago', value); + } else { + return value === 1 + ? localize('date.fromNow.years.singular', '{0} yr', value) + : localize('date.fromNow.years.plural', '{0} yrs', value); + } } export function toLocalISOString(date: Date): string { diff --git a/src/vs/base/common/iterator.ts b/src/vs/base/common/iterator.ts index 9287a6eaebf..6c87c85cf48 100644 --- a/src/vs/base/common/iterator.ts +++ b/src/vs/base/common/iterator.ts @@ -125,6 +125,19 @@ export module Iterator { }; } + export function some(iterator: Iterator | NativeIterator, fn: (t: T) => boolean): boolean { + while (true) { + const element = iterator.next(); + if (element.done) { + return false; + } + + if (fn(element.value)) { + return true; + } + } + } + export function forEach(iterator: Iterator, fn: (t: T) => void): void { for (let next = iterator.next(); !next.done; next = iterator.next()) { fn(next.value); diff --git a/src/vs/base/node/encoding.ts b/src/vs/base/node/encoding.ts index 4a9462c646a..7cf834ece2f 100644 --- a/src/vs/base/node/encoding.ts +++ b/src/vs/base/node/encoding.ts @@ -142,10 +142,6 @@ export function decode(buffer: Buffer, encoding: string): string { return iconv.decode(buffer, toNodeEncoding(encoding)); } -export function encode(content: string | Buffer, encoding: string, options?: { addBOM?: boolean }): Buffer { - return iconv.encode(content as string /* TODO report into upstream typings */, toNodeEncoding(encoding), options); -} - export function encodingExists(encoding: string): boolean { return iconv.encodingExists(toNodeEncoding(encoding)); } diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index 6cf71160c50..491c760df4b 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -14,7 +14,6 @@ import { promisify } from 'util'; import { isRootOrDriveLetter } from 'vs/base/common/extpath'; import { generateUuid } from 'vs/base/common/uuid'; import { normalizeNFC } from 'vs/base/common/normalization'; -import { encode } from 'vs/base/node/encoding'; // See https://github.com/Microsoft/vscode/issues/30180 const WIN32_MAX_FILE_SIZE = 300 * 1024 * 1024; // 300 MB @@ -320,10 +319,6 @@ function ensureWriteFileQueue(queueKey: string): Queue { export interface IWriteFileOptions { mode?: number; flag?: string; - encoding?: { - charset: string; - addBOM: boolean; - }; } interface IEnsuredWriteFileOptions extends IWriteFileOptions { @@ -339,10 +334,6 @@ let canFlush = true; // // See https://github.com/nodejs/node/blob/v5.10.0/lib/fs.js#L1194 function doWriteFileAndFlush(path: string, data: string | Buffer | Uint8Array, options: IEnsuredWriteFileOptions, callback: (error: Error | null) => void): void { - if (options.encoding) { - data = encode(data instanceof Uint8Array ? Buffer.from(data) : data, options.encoding.charset, { addBOM: options.encoding.addBOM }); - } - if (!canFlush) { return fs.writeFile(path, data, { mode: options.mode, flag: options.flag }, callback); } @@ -378,10 +369,6 @@ function doWriteFileAndFlush(path: string, data: string | Buffer | Uint8Array, o export function writeFileSync(path: string, data: string | Buffer, options?: IWriteFileOptions): void { const ensuredOptions = ensureWriteOptions(options); - if (ensuredOptions.encoding) { - data = encode(data, ensuredOptions.encoding.charset, { addBOM: ensuredOptions.encoding.addBOM }); - } - if (!canFlush) { return fs.writeFileSync(path, data, { mode: ensuredOptions.mode, flag: ensuredOptions.flag }); } @@ -413,8 +400,7 @@ function ensureWriteOptions(options?: IWriteFileOptions): IEnsuredWriteFileOptio return { mode: typeof options.mode === 'number' ? options.mode : 0o666, - flag: typeof options.flag === 'string' ? options.flag : 'w', - encoding: options.encoding + flag: typeof options.flag === 'string' ? options.flag : 'w' }; } diff --git a/src/vs/base/node/stream.ts b/src/vs/base/node/stream.ts index 16b73835bd1..12ba5e5d929 100644 --- a/src/vs/base/node/stream.ts +++ b/src/vs/base/node/stream.ts @@ -3,75 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; import { VSBufferReadableStream, VSBufferReadable, VSBuffer } from 'vs/base/common/buffer'; import { Readable } from 'stream'; import { isUndefinedOrNull } from 'vs/base/common/types'; import { UTF8, UTF8_with_bom, UTF8_BOM, UTF16be, UTF16le_BOM, UTF16be_BOM, UTF16le, UTF_ENCODING } from 'vs/base/node/encoding'; -/** - * Reads a file until a matching string is found. - * - * @param file The file to read. - * @param matchingString The string to search for. - * @param chunkBytes The number of bytes to read each iteration. - * @param maximumBytesToRead The maximum number of bytes to read before giving up. - * @param callback The finished callback. - */ -export function readToMatchingString(file: string, matchingString: string, chunkBytes: number, maximumBytesToRead: number): Promise { - return new Promise((resolve, reject) => - fs.open(file, 'r', null, (err, fd) => { - if (err) { - return reject(err); - } - - function end(err: Error | null, result: string | null): void { - fs.close(fd, closeError => { - if (closeError) { - return reject(closeError); - } - - if (err && (err).code === 'EISDIR') { - return reject(err); // we want to bubble this error up (file is actually a folder) - } - - return resolve(result); - }); - } - - const buffer = Buffer.allocUnsafe(maximumBytesToRead); - let offset = 0; - - function readChunk(): void { - fs.read(fd, buffer, offset, chunkBytes, null, (err, bytesRead) => { - if (err) { - return end(err, null); - } - - if (bytesRead === 0) { - return end(null, null); - } - - offset += bytesRead; - - const newLineIndex = buffer.indexOf(matchingString); - if (newLineIndex >= 0) { - return end(null, buffer.toString('utf8').substr(0, newLineIndex)); - } - - if (offset >= maximumBytesToRead) { - return end(new Error(`Could not find ${matchingString} in first ${maximumBytesToRead} bytes of ${file}`), null); - } - - return readChunk(); - }); - } - - readChunk(); - }) - ); -} - export function streamToNodeReadable(stream: VSBufferReadableStream): Readable { return new class extends Readable { private listening = false; diff --git a/src/vs/base/parts/quickinput/browser/quickInput.ts b/src/vs/base/parts/quickinput/browser/quickInput.ts index afb94d70395..059eb29406d 100644 --- a/src/vs/base/parts/quickinput/browser/quickInput.ts +++ b/src/vs/base/parts/quickinput/browser/quickInput.ts @@ -575,6 +575,10 @@ class QuickPick extends QuickInput implements IQuickPi return this.visible ? this.ui.inputBox.hasFocus() : false; } + public focusOnInput() { + this.ui.inputBox.setFocus(); + } + onDidChangeSelection = this.onDidChangeSelectionEmitter.event; onDidTriggerItemButton = this.onDidTriggerItemButtonEmitter.event; diff --git a/src/vs/base/parts/quickinput/common/quickInput.ts b/src/vs/base/parts/quickinput/common/quickInput.ts index 8cbf10553e7..36905c59c53 100644 --- a/src/vs/base/parts/quickinput/common/quickInput.ts +++ b/src/vs/base/parts/quickinput/common/quickInput.ts @@ -113,7 +113,7 @@ export interface IInputOptions { placeHolder?: string; /** - * set to true to show a password prompt that will not show the typed value + * Controls if a password input is shown. Password input hides the typed text. */ password?: boolean; @@ -209,6 +209,8 @@ export interface IQuickPick extends IQuickInput { validationMessage: string | undefined; inputHasFocus(): boolean; + + focusOnInput(): void; } export interface IInputBox extends IQuickInput { diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index f6966b2772c..4ba8c87cb98 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -24,7 +24,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IStateService } from 'vs/platform/state/node/state'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IURLService, IOpenURLOptions } from 'vs/platform/url/common/url'; +import { IURLService } from 'vs/platform/url/common/url'; import { URLHandlerChannelClient, URLHandlerRouter } from 'vs/platform/url/common/urlIpc'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService, combinedAppender, LogAppender } from 'vs/platform/telemetry/common/telemetryUtils'; @@ -73,10 +73,10 @@ import { IDiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsSer import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc'; import { IElectronMainService, ElectronMainService } from 'vs/platform/electron/electron-main/electronMainService'; import { ISharedProcessMainService, SharedProcessMainService } from 'vs/platform/ipc/electron-main/sharedProcessMainService'; -import { assign } from 'vs/base/common/objects'; import { IDialogMainService, DialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; import { withNullAsUndefined } from 'vs/base/common/types'; import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv'; +import { coalesce } from 'vs/base/common/arrays'; export class CodeApplication extends Disposable { @@ -395,7 +395,7 @@ export class CodeApplication extends Disposable { const windows = appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, electronIpcServer, sharedProcessClient)); // Post Open Windows Tasks - appInstantiationService.invokeFunction(this.afterWindowOpen.bind(this)); + appInstantiationService.invokeFunction(accessor => this.afterWindowOpen(accessor)); // Tracing: Stop tracing after windows are ready if enabled if (this.environmentService.args.trace) { @@ -575,9 +575,8 @@ export class CodeApplication extends Disposable { electronIpcServer.registerChannel('logger', loggerChannel); sharedProcessClient.then(client => client.registerChannel('logger', loggerChannel)); - const windowsMainService = this.windowsMainService = accessor.get(IWindowsMainService); - // ExtensionHost Debug broadcast service + const windowsMainService = this.windowsMainService = accessor.get(IWindowsMainService); electronIpcServer.registerChannel(ExtensionHostDebugBroadcastChannel.ChannelName, new ElectronExtensionHostDebugBroadcastChannel(windowsMainService)); // Signal phase: ready (services set) @@ -586,47 +585,67 @@ export class CodeApplication extends Disposable { // Propagate to clients this.dialogMainService = accessor.get(IDialogMainService); - // Create a URL handler to open file URIs in the active window + // Check for initial URLs to handle from protocol link invocations const environmentService = accessor.get(IEnvironmentService); + const pendingWindowOpenablesFromProtocolLinks: IWindowOpenable[] = []; + const pendingProtocolLinksToHandle = coalesce([ + + // Windows/Linux: protocol handler invokes CLI with --open-url + ...environmentService.args['open-url'] ? environmentService.args._urls || [] : [], + + // macOS: open-url events + ...((global).getOpenUrls() || []) as string[] + ].map(pendingUrlToHandle => { + try { + return URI.parse(pendingUrlToHandle); + } catch (error) { + return undefined; + } + })).filter(pendingUriToHandle => { + // filter out any protocol link that wants to open as window so that + // we open the right set of windows on startup and not restore the + // previous workspace too. + const windowOpenable = this.getWindowOpenableFromProtocolLink(pendingUriToHandle); + if (windowOpenable) { + pendingWindowOpenablesFromProtocolLinks.push(windowOpenable); + + return false; + } + + return true; + }); + + // Create a URL handler to open file URIs in the active window + const app = this; urlService.registerHandler({ - async handleURL(uri: URI, options?: IOpenURLOptions): Promise { + async handleURL(uri: URI): Promise { - // Catch file/remote URLs - if ((uri.authority === Schemas.file || uri.authority === Schemas.vscodeRemote) && !!uri.path) { - const cli = assign(Object.create(null), environmentService.args); - const urisToOpen: IWindowOpenable[] = []; + // Check for URIs to open in window + const windowOpenableFromProtocolLink = app.getWindowOpenableFromProtocolLink(uri); + if (windowOpenableFromProtocolLink) { + windowsMainService.open({ + context: OpenContext.API, + cli: { ...environmentService.args }, + urisToOpen: [windowOpenableFromProtocolLink], + gotoLineMode: true + }); - // File path - if (uri.authority === Schemas.file) { - // we configure as fileUri, but later validation will - // make sure to open as folder or workspace if possible - urisToOpen.push({ fileUri: URI.file(uri.fsPath) }); - } + return true; + } - // Remote path - else { - // Example conversion: - // From: vscode://vscode-remote/wsl+ubuntu/mnt/c/GitDevelopment/monaco - // To: vscode-remote://wsl+ubuntu/mnt/c/GitDevelopment/monaco - const secondSlash = uri.path.indexOf(posix.sep, 1 /* skip over the leading slash */); - if (secondSlash !== -1) { - const authority = uri.path.substring(1, secondSlash); - const path = uri.path.substring(secondSlash); - const remoteUri = URI.from({ scheme: Schemas.vscodeRemote, authority, path, query: uri.query, fragment: uri.fragment }); + // If we have not yet handled the URI and we have no window opened (macOS only) + // we first open a window and then try to open that URI within that window + if (isMacintosh && windowsMainService.getWindowCount() === 0) { + const [window] = windowsMainService.open({ + context: OpenContext.API, + cli: { ...environmentService.args }, + forceEmpty: true, + gotoLineMode: true + }); - if (hasWorkspaceFileExtension(path)) { - urisToOpen.push({ workspaceUri: remoteUri }); - } else { - urisToOpen.push({ folderUri: remoteUri }); - } - } - } + await window.ready(); - if (urisToOpen.length > 0) { - windowsMainService.open({ context: OpenContext.API, cli, urisToOpen, gotoLineMode: true }); - - return true; - } + return urlService.open(uri); } return false; @@ -638,37 +657,13 @@ export class CodeApplication extends Disposable { const activeWindowRouter = new StaticRouter(ctx => activeWindowManager.getActiveClientId().then(id => ctx === id)); const urlHandlerRouter = new URLHandlerRouter(activeWindowRouter); const urlHandlerChannel = electronIpcServer.getChannel('urlHandler', urlHandlerRouter); - const multiplexURLHandler = new URLHandlerChannelClient(urlHandlerChannel); - - // On Mac, Code can be running without any open windows, so we must create a window to handle urls, - // if there is none - if (isMacintosh) { - urlService.registerHandler({ - async handleURL(uri: URI, options?: IOpenURLOptions): Promise { - if (windowsMainService.getWindowCount() === 0) { - const cli = { ...environmentService.args }; - const [window] = windowsMainService.open({ context: OpenContext.API, cli, forceEmpty: true, gotoLineMode: true }); - - await window.ready(); - - return urlService.open(uri); - } - - return false; - } - }); - } - - // Register the multiple URL handler - urlService.registerHandler(multiplexURLHandler); + urlService.registerHandler(new URLHandlerChannelClient(urlHandlerChannel)); // Watch Electron URLs and forward them to the UrlService - const args = this.environmentService.args; - const urls = args['open-url'] ? args._urls : []; - const urlListener = new ElectronURLListener(urls || [], urlService, windowsMainService, this.environmentService); - this._register(urlListener); + this._register(new ElectronURLListener(pendingProtocolLinksToHandle, urlService, windowsMainService, this.environmentService)); // Open our first window + const args = this.environmentService.args; const macOpenFiles: string[] = (global).macOpenFiles; const context = !!process.env['VSCODE_CLI'] ? OpenContext.CLI : OpenContext.DESKTOP; const hasCliArgs = args._.length; @@ -677,6 +672,19 @@ export class CodeApplication extends Disposable { const noRecentEntry = args['skip-add-to-recently-opened'] === true; const waitMarkerFileURI = args.wait && args.waitMarkerFilePath ? URI.file(args.waitMarkerFilePath) : undefined; + // check for a pending window to open from URI + // e.g. when running code with --open-uri from + // a protocol handler + if (pendingWindowOpenablesFromProtocolLinks.length > 0) { + return windowsMainService.open({ + context, + cli: args, + urisToOpen: pendingWindowOpenablesFromProtocolLinks, + gotoLineMode: true, + initialStartup: true + }); + } + // new window if "-n" or "--remote" was used without paths if ((args['new-window'] || args.remote) && !hasCliArgs && !hasFolderURIs && !hasFileURIs) { return windowsMainService.open({ @@ -698,7 +706,6 @@ export class CodeApplication extends Disposable { urisToOpen: macOpenFiles.map(file => this.getWindowOpenableFromPathSync(file)), noRecentEntry, waitMarkerFileURI, - gotoLineMode: false, initialStartup: true }); } @@ -716,6 +723,40 @@ export class CodeApplication extends Disposable { }); } + private getWindowOpenableFromProtocolLink(uri: URI): IWindowOpenable | undefined { + if (!uri.path) { + return undefined; + } + + // File path + if (uri.authority === Schemas.file) { + // we configure as fileUri, but later validation will + // make sure to open as folder or workspace if possible + return { fileUri: URI.file(uri.fsPath) }; + } + + // Remote path + else if (uri.authority === Schemas.vscodeRemote) { + // Example conversion: + // From: vscode://vscode-remote/wsl+ubuntu/mnt/c/GitDevelopment/monaco + // To: vscode-remote://wsl+ubuntu/mnt/c/GitDevelopment/monaco + const secondSlash = uri.path.indexOf(posix.sep, 1 /* skip over the leading slash */); + if (secondSlash !== -1) { + const authority = uri.path.substring(1, secondSlash); + const path = uri.path.substring(secondSlash); + const remoteUri = URI.from({ scheme: Schemas.vscodeRemote, authority, path, query: uri.query, fragment: uri.fragment }); + + if (hasWorkspaceFileExtension(path)) { + return { workspaceUri: remoteUri }; + } else { + return { folderUri: remoteUri }; + } + } + } + + return undefined; + } + private getWindowOpenableFromPathSync(path: string): IWindowOpenable { try { const fileStat = statSync(path); @@ -734,6 +775,7 @@ export class CodeApplication extends Disposable { } private afterWindowOpen(accessor: ServicesAccessor): void { + // Signal phase: after window open this.lifecycleMainService.phase = LifecycleMainPhase.AfterWindowOpen; @@ -763,7 +805,7 @@ class ElectronExtensionHostDebugBroadcastChannel extends ExtensionHost super(); } - call(ctx: TContext, command: string, arg?: any): Promise { + async call(ctx: TContext, command: string, arg?: any): Promise { if (command === 'openExtensionDevelopmentHostWindow') { const env = arg[1]; const pargs = parseArgs(arg[0], OPTIONS); @@ -775,7 +817,6 @@ class ElectronExtensionHostDebugBroadcastChannel extends ExtensionHost userEnv: Object.keys(env).length > 0 ? env : undefined }); } - return Promise.resolve(); } else { return super.call(ctx, command, arg); } diff --git a/src/vs/code/node/paths.ts b/src/vs/code/node/paths.ts index f605a4526f3..6bdcf79ed03 100644 --- a/src/vs/code/node/paths.ts +++ b/src/vs/code/node/paths.ts @@ -19,8 +19,8 @@ export function validatePaths(args: ParsedArgs): ParsedArgs { args._ = []; } + // Normalize paths and watch out for goto line mode if (!args['remote']) { - // Normalize paths and watch out for goto line mode const paths = doValidatePaths(args._, args.goto); args._ = paths; } diff --git a/src/vs/editor/browser/controller/coreCommands.ts b/src/vs/editor/browser/controller/coreCommands.ts index 567b9cdc08a..ca00039a592 100644 --- a/src/vs/editor/browser/controller/coreCommands.ts +++ b/src/vs/editor/browser/controller/coreCommands.ts @@ -24,7 +24,7 @@ import { MenuId } from 'vs/platform/actions/common/actions'; import { ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeybindingWeight, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; const CORE_WEIGHT = KeybindingWeight.EditorCore; @@ -1530,6 +1530,26 @@ export namespace CoreNavigationCommands { }); } +const columnSelectionCondition = ContextKeyExpr.and( + EditorContextKeys.textInputFocus, + EditorContextKeys.columnSelection +); +function registerColumnSelection(id: string, keybinding: number): void { + KeybindingsRegistry.registerKeybindingRule({ + id: id, + primary: keybinding, + when: columnSelectionCondition, + weight: CORE_WEIGHT + 1 + }); +} + +registerColumnSelection(CoreNavigationCommands.CursorColumnSelectLeft.id, KeyMod.Shift | KeyCode.LeftArrow); +registerColumnSelection(CoreNavigationCommands.CursorColumnSelectRight.id, KeyMod.Shift | KeyCode.RightArrow); +registerColumnSelection(CoreNavigationCommands.CursorColumnSelectUp.id, KeyMod.Shift | KeyCode.UpArrow); +registerColumnSelection(CoreNavigationCommands.CursorColumnSelectPageUp.id, KeyMod.Shift | KeyCode.PageUp); +registerColumnSelection(CoreNavigationCommands.CursorColumnSelectDown.id, KeyMod.Shift | KeyCode.DownArrow); +registerColumnSelection(CoreNavigationCommands.CursorColumnSelectPageDown.id, KeyMod.Shift | KeyCode.PageDown); + /** * A command that will: * 1. invoke a command on the focused editor. diff --git a/src/vs/editor/browser/controller/mouseHandler.ts b/src/vs/editor/browser/controller/mouseHandler.ts index 0e5f6b0d454..5bab90e5eb1 100644 --- a/src/vs/editor/browser/controller/mouseHandler.ts +++ b/src/vs/editor/browser/controller/mouseHandler.ts @@ -352,6 +352,7 @@ class MouseDownOperation extends Disposable { if (!options.get(EditorOption.readOnly) && options.get(EditorOption.dragAndDrop) + && !options.get(EditorOption.columnSelection) && !this._mouseState.altKey // we don't support multiple mouse && e.detail < 2 // only single click on a selection can work && !this._isActive // the mouse is not down yet diff --git a/src/vs/editor/browser/view/viewController.ts b/src/vs/editor/browser/view/viewController.ts index d2eb4bdd1f1..d99af7b2b66 100644 --- a/src/vs/editor/browser/view/viewController.ts +++ b/src/vs/editor/browser/view/viewController.ts @@ -133,7 +133,9 @@ export class ViewController { } public dispatchMouse(data: IMouseDispatchData): void { - const selectionClipboardIsOn = (platform.isLinux && this.configuration.options.get(EditorOption.selectionClipboard)); + const options = this.configuration.options; + const selectionClipboardIsOn = (platform.isLinux && options.get(EditorOption.selectionClipboard)); + const columnSelection = options.get(EditorOption.columnSelection); if (data.middleButton && !selectionClipboardIsOn) { this._columnSelect(data.position, data.mouseColumn, data.inSelectionMode); } else if (data.startedOnLineNumbers) { @@ -196,7 +198,11 @@ export class ViewController { if (data.altKey) { this._columnSelect(data.position, data.mouseColumn, true); } else { - this._moveToSelect(data.position); + if (columnSelection) { + this._columnSelect(data.position, data.mouseColumn, true); + } else { + this._moveToSelect(data.position); + } } } else { this.moveTo(data.position); diff --git a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts b/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts index c22e5e84ad0..a3a16436846 100644 --- a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts +++ b/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts @@ -369,7 +369,7 @@ class Widget { return { fitsAbove, - aboveTop: Math.max(aboveTop, TOP_PADDING), + aboveTop: aboveTop, aboveLeft, fitsBelow, belowTop, diff --git a/src/vs/editor/browser/viewParts/lines/rangeUtil.ts b/src/vs/editor/browser/viewParts/lines/rangeUtil.ts index f84a79df8d4..7718ae4003a 100644 --- a/src/vs/editor/browser/viewParts/lines/rangeUtil.ts +++ b/src/vs/editor/browser/viewParts/lines/rangeUtil.ts @@ -121,6 +121,13 @@ export class RangeUtil { startChildIndex = Math.min(max, Math.max(min, startChildIndex)); endChildIndex = Math.min(max, Math.max(min, endChildIndex)); + if (startChildIndex === endChildIndex && startOffset === endOffset && startOffset === 0) { + // We must find the position at the beginning of a + // To cover cases of empty s, aboid using a range and use the 's bounding box + const clientRects = domNode.children[startChildIndex].getClientRects(); + return this._createHorizontalRangesFromClientRects(clientRects, clientRectDeltaLeft); + } + // If crossing over to a span only to select offset 0, then use the previous span's maximum offset // Chrome is buggy and doesn't handle 0 offsets well sometimes. if (startChildIndex !== endChildIndex) { diff --git a/src/vs/editor/browser/viewParts/lines/viewLine.ts b/src/vs/editor/browser/viewParts/lines/viewLine.ts index 0c1b66decdd..9c93f326a6f 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLine.ts @@ -195,7 +195,7 @@ export class ViewLine implements IVisibleLine { const endColumn = (selection.endLineNumber === lineNumber ? selection.endColumn : lineData.maxColumn); if (startColumn < endColumn) { - if (this._options.renderWhitespace !== 'selection') { + if (options.themeType === HIGH_CONTRAST || this._options.renderWhitespace !== 'selection') { actualInlineDecorations.push(new LineDecoration(startColumn, endColumn, 'inline-selected-text', InlineDecorationType.Regular)); } else { if (!selectionsOnLine) { diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.ts b/src/vs/editor/browser/viewParts/minimap/minimap.ts index 5f5c5cadd9f..d5aa95d0611 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimap.ts @@ -1149,15 +1149,15 @@ class InnerMinimap extends Disposable { this._gestureInProgress = true; this.scrollDueToTouchEvent(e); } - }); + }, { passive: false }); - this._sliderTouchMoveListener = dom.addStandardDisposableListener(this._domNode.domNode, EventType.Change, (e: GestureEvent) => { + this._sliderTouchMoveListener = dom.addDisposableListener(this._domNode.domNode, EventType.Change, (e: GestureEvent) => { e.preventDefault(); e.stopPropagation(); if (this._lastRenderData && this._gestureInProgress) { this.scrollDueToTouchEvent(e); } - }); + }, { passive: false }); this._sliderTouchEndListener = dom.addStandardDisposableListener(this._domNode.domNode, EventType.End, (e: GestureEvent) => { e.preventDefault(); diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index 5262d8003b9..c8be02dfba7 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -1666,6 +1666,7 @@ class EditorContextKeysManager extends Disposable { private readonly _editorTextFocus: IContextKey; private readonly _editorTabMovesFocus: IContextKey; private readonly _editorReadonly: IContextKey; + private readonly _editorColumnSelection: IContextKey; private readonly _hasMultipleSelections: IContextKey; private readonly _hasNonEmptySelection: IContextKey; private readonly _canUndo: IContextKey; @@ -1687,6 +1688,7 @@ class EditorContextKeysManager extends Disposable { this._editorTextFocus = EditorContextKeys.editorTextFocus.bindTo(contextKeyService); this._editorTabMovesFocus = EditorContextKeys.tabMovesFocus.bindTo(contextKeyService); this._editorReadonly = EditorContextKeys.readOnly.bindTo(contextKeyService); + this._editorColumnSelection = EditorContextKeys.columnSelection.bindTo(contextKeyService); this._hasMultipleSelections = EditorContextKeys.hasMultipleSelections.bindTo(contextKeyService); this._hasNonEmptySelection = EditorContextKeys.hasNonEmptySelection.bindTo(contextKeyService); this._canUndo = EditorContextKeys.canUndo.bindTo(contextKeyService); @@ -1714,6 +1716,7 @@ class EditorContextKeysManager extends Disposable { this._editorTabMovesFocus.set(options.get(EditorOption.tabFocusMode)); this._editorReadonly.set(options.get(EditorOption.readOnly)); + this._editorColumnSelection.set(options.get(EditorOption.columnSelection)); } private _updateFromSelection(): void { diff --git a/src/vs/editor/common/config/commonEditorConfig.ts b/src/vs/editor/common/config/commonEditorConfig.ts index af8e96020e6..ee224f1f6fd 100644 --- a/src/vs/editor/common/config/commonEditorConfig.ts +++ b/src/vs/editor/common/config/commonEditorConfig.ts @@ -523,7 +523,7 @@ const editorConfiguration: IConfigurationNode = { 'diffEditor.ignoreTrimWhitespace': { type: 'boolean', default: true, - description: nls.localize('ignoreTrimWhitespace', "Controls whether the diff editor shows changes in leading or trailing whitespace as diffs.") + description: nls.localize('ignoreTrimWhitespace', "When enabled, the diff editor ignores changes in leading or trailing whitespace.") }, 'diffEditor.renderIndicators': { type: 'boolean', diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index a187f9718e5..270e0018dff 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -324,6 +324,11 @@ export interface IEditorOptions { * Defaults to true. */ scrollPredominantAxis?: boolean; + /** + * Enable that the selection with the mouse and keys is doing column selection. + * Defaults to false. + */ + columnSelection?: boolean; /** * The modifier to be used to add multiple cursors with the mouse. * Defaults to 'alt' @@ -501,6 +506,11 @@ export interface IEditorOptions { * Defaults to 'mouseover'. */ showFoldingControls?: 'always' | 'mouseover'; + /** + * Controls whether clicking on the empty content after a folded line will unfold the line. + * Defaults to false. + */ + unfoldOnClickAfterEndOfLine?: boolean; /** * Enable highlighting of matching brackets. * Defaults to 'always'. @@ -3294,6 +3304,7 @@ export const enum EditorOption { autoSurround, codeLens, colorDecorators, + columnSelection, comments, contextmenu, copyWithSyntaxHighlighting, @@ -3314,6 +3325,7 @@ export const enum EditorOption { folding, foldingStrategy, foldingHighlight, + unfoldOnClickAfterEndOfLine, fontFamily, fontInfo, fontLigatures, @@ -3518,6 +3530,10 @@ export const EditorOptions = { EditorOption.colorDecorators, 'colorDecorators', true, { description: nls.localize('colorDecorators', "Controls whether the editor should render the inline color decorators and color picker.") } )), + columnSelection: register(new EditorBooleanOption( + EditorOption.columnSelection, 'columnSelection', false, + { description: nls.localize('columnSelection', "Enable that the selection with the mouse and keys is doing column selection.") } + )), comments: register(new EditorComments()), contextmenu: register(new EditorBooleanOption( EditorOption.contextmenu, 'contextmenu', true, @@ -3609,6 +3625,10 @@ export const EditorOptions = { EditorOption.foldingHighlight, 'foldingHighlight', true, { description: nls.localize('foldingHighlight', "Controls whether the editor should highlight folded ranges.") } )), + unfoldOnClickAfterEndOfLine: register(new EditorBooleanOption( + EditorOption.unfoldOnClickAfterEndOfLine, 'unfoldOnClickAfterEndOfLine', false, + { description: nls.localize('unfoldOnClickAfterEndOfLine', "Controls whether clicking on the empty content after a folded line will unfold the line.") } + )), fontFamily: register(new EditorStringOption( EditorOption.fontFamily, 'fontFamily', EDITOR_FONT_DEFAULTS.fontFamily, { description: nls.localize('fontFamily', "Controls the font family.") } diff --git a/src/vs/editor/common/controller/cursor.ts b/src/vs/editor/common/controller/cursor.ts index cd488b900ac..883abf19c6d 100644 --- a/src/vs/editor/common/controller/cursor.ts +++ b/src/vs/editor/common/controller/cursor.ts @@ -297,7 +297,7 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { return this._cursors.getAll(); } - public setStates(source: string, reason: CursorChangeReason, states: PartialCursorState[] | null): void { + public setStates(source: string, reason: CursorChangeReason, states: PartialCursorState[] | null): boolean { if (states !== null && states.length > Cursor.MAX_CURSOR_COUNT) { states = states.slice(0, Cursor.MAX_CURSOR_COUNT); this._onDidReachMaxCursorCount.fire(undefined); @@ -311,7 +311,7 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { this._validateAutoClosedActions(); - this._emitStateChangedIfNecessary(source, reason, oldState); + return this._emitStateChangedIfNecessary(source, reason, oldState); } public setColumnSelectData(columnSelectData: IColumnSelectData): void { @@ -411,7 +411,9 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { } else { if (this._hasFocus && e.resultingSelection && e.resultingSelection.length > 0) { const cursorState = CursorState.fromModelSelections(e.resultingSelection); - this.setStates('modelChange', e.isUndoing ? CursorChangeReason.Undo : e.isRedoing ? CursorChangeReason.Redo : CursorChangeReason.RecoverFromMarkers, cursorState); + if (this.setStates('modelChange', e.isUndoing ? CursorChangeReason.Undo : e.isRedoing ? CursorChangeReason.Redo : CursorChangeReason.RecoverFromMarkers, cursorState)) { + this._revealRange('modelChange', RevealTarget.Primary, viewEvents.VerticalRevealType.Simple, true, editorCommon.ScrollType.Smooth); + } } else { const selectionsFromMarkers = this._cursors.readSelectionFromMarkers(); this.setStates('modelChange', CursorChangeReason.RecoverFromMarkers, CursorState.fromModelSelections(selectionsFromMarkers)); diff --git a/src/vs/editor/common/editorContextKeys.ts b/src/vs/editor/common/editorContextKeys.ts index f15b8091073..01668420569 100644 --- a/src/vs/editor/common/editorContextKeys.ts +++ b/src/vs/editor/common/editorContextKeys.ts @@ -23,6 +23,7 @@ export namespace EditorContextKeys { export const textInputFocus = new RawContextKey('textInputFocus', false); export const readOnly = new RawContextKey('editorReadonly', false); + export const columnSelection = new RawContextKey('editorColumnSelection', false); export const writable: ContextKeyExpr = readOnly.toNegated(); export const hasNonEmptySelection = new RawContextKey('editorHasSelection', false); export const hasOnlyEmptySelection: ContextKeyExpr = hasNonEmptySelection.toNegated(); diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 1f05f099c7e..832b40f06b5 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -1083,7 +1083,7 @@ export interface ITextModel { * @param cursorStateComputer A callback that can compute the resulting cursors state after the edit operations have been executed. * @return The cursor state returned by the `cursorStateComputer`. */ - pushEditOperations(beforeCursorState: Selection[], editOperations: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer): Selection[] | null; + pushEditOperations(beforeCursorState: Selection[] | null, editOperations: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer): Selection[] | null; /** * Change the end of line sequence. This is the preferred way of @@ -1099,11 +1099,6 @@ export interface ITextModel { */ applyEdits(operations: IIdentifiedSingleEditOperation[]): IValidEditOperation[]; - /** - * @internal - */ - _applyEdits(edits: IValidEditOperations[], isUndoing: boolean, isRedoing: boolean, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): IValidEditOperations[]; - /** * Change the end of line sequence without recording in the undo stack. * This can have dire consequences on the undo stack! See @pushEOL for the preferred way. @@ -1113,7 +1108,7 @@ export interface ITextModel { /** * @internal */ - _setEOL(eol: EndOfLineSequence, isUndoing: boolean, isRedoing: boolean, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): void; + _applyUndoRedoEdits(edits: IValidEditOperations[], eol: EndOfLineSequence, isUndoing: boolean, isRedoing: boolean, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): IValidEditOperations[]; /** * Undo edit operations until the first previous stop point created by `pushStackElement`. diff --git a/src/vs/editor/common/model/editStack.ts b/src/vs/editor/common/model/editStack.ts index 1e6c83563d8..c0aee2abe10 100644 --- a/src/vs/editor/common/model/editStack.ts +++ b/src/vs/editor/common/model/editStack.ts @@ -8,41 +8,50 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { Selection } from 'vs/editor/common/core/selection'; import { EndOfLineSequence, ICursorStateComputer, IIdentifiedSingleEditOperation, IValidEditOperation, ITextModel, IValidEditOperations } from 'vs/editor/common/model'; import { TextModel } from 'vs/editor/common/model/textModel'; -import { IUndoRedoService, IUndoRedoElement, IUndoRedoContext } from 'vs/platform/undoRedo/common/undoRedo'; +import { IUndoRedoService, IResourceUndoRedoElement, UndoRedoElementType, IWorkspaceUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo'; import { URI } from 'vs/base/common/uri'; +import { getComparisonKey as uriGetComparisonKey } from 'vs/base/common/resources'; -class EditStackElement implements IUndoRedoElement { +export class EditStackElement implements IResourceUndoRedoElement { + public readonly type = UndoRedoElementType.Resource; public readonly label: string; private _isOpen: boolean; - private readonly _model: TextModel; + public readonly model: ITextModel; private readonly _beforeVersionId: number; - private readonly _beforeCursorState: Selection[]; + private readonly _beforeEOL: EndOfLineSequence; + private readonly _beforeCursorState: Selection[] | null; private _afterVersionId: number; + private _afterEOL: EndOfLineSequence; private _afterCursorState: Selection[] | null; private _edits: IValidEditOperations[]; - public get resources(): readonly URI[] { - return [this._model.uri]; + public get resource(): URI { + return this.model.uri; } - constructor(model: TextModel, beforeVersionId: number, beforeCursorState: Selection[], afterVersionId: number, afterCursorState: Selection[] | null, operations: IValidEditOperation[]) { + constructor(model: ITextModel, beforeCursorState: Selection[] | null) { this.label = nls.localize('edit', "Typing"); this._isOpen = true; - this._model = model; - this._beforeVersionId = beforeVersionId; + this.model = model; + this._beforeVersionId = this.model.getAlternativeVersionId(); + this._beforeEOL = getModelEOL(this.model); this._beforeCursorState = beforeCursorState; - this._afterVersionId = afterVersionId; - this._afterCursorState = afterCursorState; - this._edits = [{ operations: operations }]; + this._afterVersionId = this._beforeVersionId; + this._afterEOL = this._beforeEOL; + this._afterCursorState = this._beforeCursorState; + this._edits = []; } - public isOpen(): boolean { - return this._isOpen; + public canAppend(model: ITextModel): boolean { + return (this._isOpen && this.model === model); } - public append(operations: IValidEditOperation[], afterVersionId: number, afterCursorState: Selection[] | null): void { - this._edits.push({ operations: operations }); + public append(model: ITextModel, operations: IValidEditOperation[], afterEOL: EndOfLineSequence, afterVersionId: number, afterCursorState: Selection[] | null): void { + if (operations.length > 0) { + this._edits.push({ operations: operations }); + } + this._afterEOL = afterEOL; this._afterVersionId = afterVersionId; this._afterCursorState = afterCursorState; } @@ -51,20 +60,83 @@ class EditStackElement implements IUndoRedoElement { this._isOpen = false; } - undo(ctx: IUndoRedoContext): void { + public undo(): void { this._isOpen = false; this._edits.reverse(); - this._edits = this._model._applyEdits(this._edits, true, false, this._beforeVersionId, this._beforeCursorState); + this._edits = this.model._applyUndoRedoEdits(this._edits, this._beforeEOL, true, false, this._beforeVersionId, this._beforeCursorState); } - redo(ctx: IUndoRedoContext): void { - this._isOpen = false; + public redo(): void { this._edits.reverse(); - this._edits = this._model._applyEdits(this._edits, false, true, this._afterVersionId, this._afterCursorState); + this._edits = this.model._applyUndoRedoEdits(this._edits, this._afterEOL, false, true, this._afterVersionId, this._afterCursorState); + } +} + +export class MultiModelEditStackElement implements IWorkspaceUndoRedoElement { + + public readonly type = UndoRedoElementType.Workspace; + public readonly label: string; + private _isOpen: boolean; + + private readonly _editStackElementsArr: EditStackElement[]; + private readonly _editStackElementsMap: Map; + + public get resources(): readonly URI[] { + return this._editStackElementsArr.map(editStackElement => editStackElement.model.uri); } - invalidate(resource: URI): void { - // nothing to do + constructor( + label: string, + editStackElements: EditStackElement[] + ) { + this.label = label; + this._isOpen = true; + this._editStackElementsArr = editStackElements.slice(0); + this._editStackElementsMap = new Map(); + for (const editStackElement of this._editStackElementsArr) { + const key = uriGetComparisonKey(editStackElement.model.uri); + this._editStackElementsMap.set(key, editStackElement); + } + } + + public canAppend(model: ITextModel): boolean { + if (!this._isOpen) { + return false; + } + const key = uriGetComparisonKey(model.uri); + if (this._editStackElementsMap.has(key)) { + const editStackElement = this._editStackElementsMap.get(key)!; + return editStackElement.canAppend(model); + } + return false; + } + + public append(model: ITextModel, operations: IValidEditOperation[], afterEOL: EndOfLineSequence, afterVersionId: number, afterCursorState: Selection[] | null): void { + const key = uriGetComparisonKey(model.uri); + const editStackElement = this._editStackElementsMap.get(key)!; + editStackElement.append(model, operations, afterEOL, afterVersionId, afterCursorState); + } + + public close(): void { + this._isOpen = false; + } + + public undo(): void { + this._isOpen = false; + + for (const editStackElement of this._editStackElementsArr) { + editStackElement.undo(); + } + } + + public redo(): void { + for (const editStackElement of this._editStackElementsArr) { + editStackElement.redo(); + } + } + + public split(): IResourceUndoRedoElement[] { + return this._editStackElementsArr; } } @@ -77,46 +149,11 @@ function getModelEOL(model: ITextModel): EndOfLineSequence { } } -class EOLStackElement implements IUndoRedoElement { - - public readonly label: string; - private readonly _model: TextModel; - private readonly _beforeVersionId: number; - private readonly _afterVersionId: number; - private _eol: EndOfLineSequence; - - public get resources(): readonly URI[] { - return [this._model.uri]; +function isKnownStackElement(element: IResourceUndoRedoElement | IWorkspaceUndoRedoElement | null): element is EditStackElement | MultiModelEditStackElement { + if (!element) { + return false; } - - constructor(model: TextModel, beforeVersionId: number, afterVersionId: number, eol: EndOfLineSequence) { - this.label = nls.localize('eol', "Change End Of Line Sequence"); - this._model = model; - this._beforeVersionId = beforeVersionId; - this._afterVersionId = afterVersionId; - this._eol = eol; - } - - undo(ctx: IUndoRedoContext): void { - const redoEOL = getModelEOL(this._model); - this._model._setEOL(this._eol, true, false, this._beforeVersionId, null); - this._eol = redoEOL; - } - - redo(ctx: IUndoRedoContext): void { - const undoEOL = getModelEOL(this._model); - this._model._setEOL(this._eol, false, true, this._afterVersionId, null); - this._eol = undoEOL; - } - - invalidate(resource: URI): void { - // nothing to do - } -} - -export interface IUndoRedoResult { - selections: Selection[] | null; - recordedVersionId: number; + return ((element instanceof EditStackElement) || (element instanceof MultiModelEditStackElement)); } export class EditStack { @@ -131,7 +168,7 @@ export class EditStack { public pushStackElement(): void { const lastElement = this._undoRedoService.getLastElement(this._model.uri); - if (lastElement && lastElement instanceof EditStackElement) { + if (isKnownStackElement(lastElement)) { lastElement.close(); } } @@ -140,32 +177,27 @@ export class EditStack { this._undoRedoService.removeElements(this._model.uri); } - public pushEOL(eol: EndOfLineSequence): void { - const beforeVersionId = this._model.getAlternativeVersionId(); - const inverseEOL = getModelEOL(this._model); - this._model.setEOL(eol); - const afterVersionId = this._model.getAlternativeVersionId(); - + private _getOrCreateEditStackElement(beforeCursorState: Selection[] | null): EditStackElement | MultiModelEditStackElement { const lastElement = this._undoRedoService.getLastElement(this._model.uri); - if (lastElement && lastElement instanceof EditStackElement) { - lastElement.close(); + if (isKnownStackElement(lastElement) && lastElement.canAppend(this._model)) { + return lastElement; } - this._undoRedoService.pushElement(new EOLStackElement(this._model, inverseEOL, beforeVersionId, afterVersionId)); + const newElement = new EditStackElement(this._model, beforeCursorState); + this._undoRedoService.pushElement(newElement); + return newElement; } - public pushEditOperation(beforeCursorState: Selection[], editOperations: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer | null): Selection[] | null { - const beforeVersionId = this._model.getAlternativeVersionId(); + public pushEOL(eol: EndOfLineSequence): void { + const editStackElement = this._getOrCreateEditStackElement(null); + this._model.setEOL(eol); + editStackElement.append(this._model, [], getModelEOL(this._model), this._model.getAlternativeVersionId(), null); + } + + public pushEditOperation(beforeCursorState: Selection[] | null, editOperations: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer | null): Selection[] | null { + const editStackElement = this._getOrCreateEditStackElement(beforeCursorState); const inverseEditOperations = this._model.applyEdits(editOperations); - const afterVersionId = this._model.getAlternativeVersionId(); const afterCursorState = EditStack._computeCursorState(cursorStateComputer, inverseEditOperations); - - const lastElement = this._undoRedoService.getLastElement(this._model.uri); - if (lastElement && lastElement instanceof EditStackElement && lastElement.isOpen()) { - lastElement.append(inverseEditOperations, afterVersionId, afterCursorState); - } else { - this._undoRedoService.pushElement(new EditStackElement(this._model, beforeVersionId, beforeCursorState, afterVersionId, afterCursorState, inverseEditOperations)); - } - + editStackElement.append(this._model, inverseEditOperations, getModelEOL(this._model), this._model.getAlternativeVersionId(), afterCursorState); return afterCursorState; } diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 0e144a9cae2..82c4732220f 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -37,7 +37,6 @@ import { Color } from 'vs/base/common/color'; import { Constants } from 'vs/base/common/uint'; import { EditorTheme } from 'vs/editor/common/view/viewContext'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; -import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; function createTextBufferBuilder() { return new PieceTreeTextBufferBuilder(); @@ -189,10 +188,6 @@ export class TextModel extends Disposable implements model.ITextModel { largeFileOptimizations: EDITOR_MODEL_DEFAULTS.largeFileOptimizations, }; - public static createFromString(text: string, options: model.ITextModelCreationOptions = TextModel.DEFAULT_CREATION_OPTIONS, languageIdentifier: LanguageIdentifier | null = null, uri: URI | null = null): TextModel { - return new TextModel(text, options, languageIdentifier, uri, new UndoRedoService()); - } - public static resolveOptions(textBuffer: model.ITextBuffer, options: model.ITextModelCreationOptions): model.TextModelResolvedOptions { if (options.detectIndentation) { const guessedIndentation = guessIndentation(textBuffer, options.tabSize, options.insertSpaces); @@ -499,21 +494,6 @@ export class TextModel extends Disposable implements model.ITextModel { ); } - _setEOL(eol: model.EndOfLineSequence, isUndoing: boolean, isRedoing: boolean, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): void { - try { - this._onDidChangeDecorations.beginDeferredEmit(); - this._eventEmitter.beginDeferredEmit(); - this._isUndoing = isUndoing; - this._isRedoing = isRedoing; - this.setEOL(eol); - this._overwriteAlternativeVersionId(resultingAlternativeVersionId); - } finally { - this._isUndoing = false; - this._eventEmitter.endDeferredEmit(resultingSelection); - this._onDidChangeDecorations.endDeferredEmit(); - } - } - private _onBeforeEOLChange(): void { // Ensure all decorations get their `range` set. const versionId = this.getVersionId(); @@ -1207,7 +1187,7 @@ export class TextModel extends Disposable implements model.ITextModel { return result; } - public pushEditOperations(beforeCursorState: Selection[], editOperations: model.IIdentifiedSingleEditOperation[], cursorStateComputer: model.ICursorStateComputer | null): Selection[] | null { + public pushEditOperations(beforeCursorState: Selection[] | null, editOperations: model.IIdentifiedSingleEditOperation[], cursorStateComputer: model.ICursorStateComputer | null): Selection[] | null { try { this._onDidChangeDecorations.beginDeferredEmit(); this._eventEmitter.beginDeferredEmit(); @@ -1218,7 +1198,7 @@ export class TextModel extends Disposable implements model.ITextModel { } } - private _pushEditOperations(beforeCursorState: Selection[], editOperations: model.ValidAnnotatedEditOperation[], cursorStateComputer: model.ICursorStateComputer | null): Selection[] | null { + private _pushEditOperations(beforeCursorState: Selection[] | null, editOperations: model.ValidAnnotatedEditOperation[], cursorStateComputer: model.ICursorStateComputer | null): Selection[] | null { if (this._options.trimAutoWhitespace && this._trimAutoWhitespaceLines) { // Go through each saved line number and insert a trim whitespace edit // if it is safe to do so (no conflicts with other edits). @@ -1233,22 +1213,24 @@ export class TextModel extends Disposable implements model.ITextModel { // Sometimes, auto-formatters change ranges automatically which can cause undesired auto whitespace trimming near the cursor // We'll use the following heuristic: if the edits occur near the cursor, then it's ok to trim auto whitespace let editsAreNearCursors = true; - for (let i = 0, len = beforeCursorState.length; i < len; i++) { - let sel = beforeCursorState[i]; - let foundEditNearSel = false; - for (let j = 0, lenJ = incomingEdits.length; j < lenJ; j++) { - let editRange = incomingEdits[j].range; - let selIsAbove = editRange.startLineNumber > sel.endLineNumber; - let selIsBelow = sel.startLineNumber > editRange.endLineNumber; - if (!selIsAbove && !selIsBelow) { - foundEditNearSel = true; + if (beforeCursorState) { + for (let i = 0, len = beforeCursorState.length; i < len; i++) { + let sel = beforeCursorState[i]; + let foundEditNearSel = false; + for (let j = 0, lenJ = incomingEdits.length; j < lenJ; j++) { + let editRange = incomingEdits[j].range; + let selIsAbove = editRange.startLineNumber > sel.endLineNumber; + let selIsBelow = sel.startLineNumber > editRange.endLineNumber; + if (!selIsAbove && !selIsBelow) { + foundEditNearSel = true; + break; + } + } + if (!foundEditNearSel) { + editsAreNearCursors = false; break; } } - if (!foundEditNearSel) { - editsAreNearCursors = false; - break; - } } if (editsAreNearCursors) { @@ -1303,7 +1285,7 @@ export class TextModel extends Disposable implements model.ITextModel { return this._commandManager.pushEditOperation(beforeCursorState, editOperations, cursorStateComputer); } - _applyEdits(edits: model.IValidEditOperations[], isUndoing: boolean, isRedoing: boolean, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): model.IValidEditOperations[] { + _applyUndoRedoEdits(edits: model.IValidEditOperations[], eol: model.EndOfLineSequence, isUndoing: boolean, isRedoing: boolean, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): model.IValidEditOperations[] { try { this._onDidChangeDecorations.beginDeferredEmit(); this._eventEmitter.beginDeferredEmit(); @@ -1313,10 +1295,12 @@ export class TextModel extends Disposable implements model.ITextModel { for (let i = 0, len = edits.length; i < len; i++) { reverseEdits[i] = { operations: this.applyEdits(edits[i].operations) }; } + this.setEOL(eol); this._overwriteAlternativeVersionId(resultingAlternativeVersionId); return reverseEdits; } finally { this._isUndoing = false; + this._isRedoing = false; this._eventEmitter.endDeferredEmit(resultingSelection); this._onDidChangeDecorations.endDeferredEmit(); } diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index b7dfb8adf9b..98af16d123d 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -494,7 +494,7 @@ export interface CompletionItem { preselect?: boolean; /** * A string or snippet that should be inserted in a document when selecting - * this completion. When `falsy` the [label](#CompletionItem.label) + * this completion. * is used. */ insertText: string; @@ -635,6 +635,9 @@ export interface CodeActionList extends IDisposable { * @internal */ export interface CodeActionProvider { + + displayName?: string + /** * Provide commands for the given document and range. */ @@ -1370,7 +1373,7 @@ export interface RenameProvider { */ export interface AuthenticationSession { id: string; - accessToken(): Promise; + accessToken(): Thenable; accountName: string; } diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index 90a9fe1b996..418fe492a04 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -178,105 +178,107 @@ export enum EditorOption { autoSurround = 10, codeLens = 11, colorDecorators = 12, - comments = 13, - contextmenu = 14, - copyWithSyntaxHighlighting = 15, - cursorBlinking = 16, - cursorSmoothCaretAnimation = 17, - cursorStyle = 18, - cursorSurroundingLines = 19, - cursorSurroundingLinesStyle = 20, - cursorWidth = 21, - disableLayerHinting = 22, - disableMonospaceOptimizations = 23, - dragAndDrop = 24, - emptySelectionClipboard = 25, - extraEditorClassName = 26, - fastScrollSensitivity = 27, - find = 28, - fixedOverflowWidgets = 29, - folding = 30, - foldingStrategy = 31, - foldingHighlight = 32, - fontFamily = 33, - fontInfo = 34, - fontLigatures = 35, - fontSize = 36, - fontWeight = 37, - formatOnPaste = 38, - formatOnType = 39, - glyphMargin = 40, - gotoLocation = 41, - hideCursorInOverviewRuler = 42, - highlightActiveIndentGuide = 43, - hover = 44, - inDiffEditor = 45, - letterSpacing = 46, - lightbulb = 47, - lineDecorationsWidth = 48, - lineHeight = 49, - lineNumbers = 50, - lineNumbersMinChars = 51, - links = 52, - matchBrackets = 53, - minimap = 54, - mouseStyle = 55, - mouseWheelScrollSensitivity = 56, - mouseWheelZoom = 57, - multiCursorMergeOverlapping = 58, - multiCursorModifier = 59, - multiCursorPaste = 60, - occurrencesHighlight = 61, - overviewRulerBorder = 62, - overviewRulerLanes = 63, - padding = 64, - parameterHints = 65, - peekWidgetDefaultFocus = 66, - definitionLinkOpensInPeek = 67, - quickSuggestions = 68, - quickSuggestionsDelay = 69, - readOnly = 70, - renderControlCharacters = 71, - renderIndentGuides = 72, - renderFinalNewline = 73, - renderLineHighlight = 74, - renderValidationDecorations = 75, - renderWhitespace = 76, - revealHorizontalRightPadding = 77, - roundedSelection = 78, - rulers = 79, - scrollbar = 80, - scrollBeyondLastColumn = 81, - scrollBeyondLastLine = 82, - scrollPredominantAxis = 83, - selectionClipboard = 84, - selectionHighlight = 85, - selectOnLineNumbers = 86, - showFoldingControls = 87, - showUnused = 88, - snippetSuggestions = 89, - smoothScrolling = 90, - stopRenderingLineAfter = 91, - suggest = 92, - suggestFontSize = 93, - suggestLineHeight = 94, - suggestOnTriggerCharacters = 95, - suggestSelection = 96, - tabCompletion = 97, - useTabStops = 98, - wordSeparators = 99, - wordWrap = 100, - wordWrapBreakAfterCharacters = 101, - wordWrapBreakBeforeCharacters = 102, - wordWrapColumn = 103, - wordWrapMinified = 104, - wrappingIndent = 105, - wrappingStrategy = 106, - editorClassName = 107, - pixelRatio = 108, - tabFocusMode = 109, - layoutInfo = 110, - wrappingInfo = 111 + columnSelection = 13, + comments = 14, + contextmenu = 15, + copyWithSyntaxHighlighting = 16, + cursorBlinking = 17, + cursorSmoothCaretAnimation = 18, + cursorStyle = 19, + cursorSurroundingLines = 20, + cursorSurroundingLinesStyle = 21, + cursorWidth = 22, + disableLayerHinting = 23, + disableMonospaceOptimizations = 24, + dragAndDrop = 25, + emptySelectionClipboard = 26, + extraEditorClassName = 27, + fastScrollSensitivity = 28, + find = 29, + fixedOverflowWidgets = 30, + folding = 31, + foldingStrategy = 32, + foldingHighlight = 33, + unfoldOnClickAfterEndOfLine = 34, + fontFamily = 35, + fontInfo = 36, + fontLigatures = 37, + fontSize = 38, + fontWeight = 39, + formatOnPaste = 40, + formatOnType = 41, + glyphMargin = 42, + gotoLocation = 43, + hideCursorInOverviewRuler = 44, + highlightActiveIndentGuide = 45, + hover = 46, + inDiffEditor = 47, + letterSpacing = 48, + lightbulb = 49, + lineDecorationsWidth = 50, + lineHeight = 51, + lineNumbers = 52, + lineNumbersMinChars = 53, + links = 54, + matchBrackets = 55, + minimap = 56, + mouseStyle = 57, + mouseWheelScrollSensitivity = 58, + mouseWheelZoom = 59, + multiCursorMergeOverlapping = 60, + multiCursorModifier = 61, + multiCursorPaste = 62, + occurrencesHighlight = 63, + overviewRulerBorder = 64, + overviewRulerLanes = 65, + padding = 66, + parameterHints = 67, + peekWidgetDefaultFocus = 68, + definitionLinkOpensInPeek = 69, + quickSuggestions = 70, + quickSuggestionsDelay = 71, + readOnly = 72, + renderControlCharacters = 73, + renderIndentGuides = 74, + renderFinalNewline = 75, + renderLineHighlight = 76, + renderValidationDecorations = 77, + renderWhitespace = 78, + revealHorizontalRightPadding = 79, + roundedSelection = 80, + rulers = 81, + scrollbar = 82, + scrollBeyondLastColumn = 83, + scrollBeyondLastLine = 84, + scrollPredominantAxis = 85, + selectionClipboard = 86, + selectionHighlight = 87, + selectOnLineNumbers = 88, + showFoldingControls = 89, + showUnused = 90, + snippetSuggestions = 91, + smoothScrolling = 92, + stopRenderingLineAfter = 93, + suggest = 94, + suggestFontSize = 95, + suggestLineHeight = 96, + suggestOnTriggerCharacters = 97, + suggestSelection = 98, + tabCompletion = 99, + useTabStops = 100, + wordSeparators = 101, + wordWrap = 102, + wordWrapBreakAfterCharacters = 103, + wordWrapBreakBeforeCharacters = 104, + wordWrapColumn = 105, + wordWrapMinified = 106, + wrappingIndent = 107, + wrappingStrategy = 108, + editorClassName = 109, + pixelRatio = 110, + tabFocusMode = 111, + layoutInfo = 112, + wrappingInfo = 113 } /** diff --git a/src/vs/editor/common/viewLayout/lineDecorations.ts b/src/vs/editor/common/viewLayout/lineDecorations.ts index 367abd68f82..74428ae28d7 100644 --- a/src/vs/editor/common/viewLayout/lineDecorations.ts +++ b/src/vs/editor/common/viewLayout/lineDecorations.ts @@ -6,6 +6,7 @@ import * as strings from 'vs/base/common/strings'; import { Constants } from 'vs/base/common/uint'; import { InlineDecoration, InlineDecorationType } from 'vs/editor/common/viewModel/viewModel'; +import { LinePartMetadata } from 'vs/editor/common/viewLayout/viewLineRenderer'; export class LineDecoration { _lineDecorationBrand: void; @@ -28,8 +29,8 @@ export class LineDecoration { } public static equalsArr(a: LineDecoration[], b: LineDecoration[]): boolean { - let aLen = a.length; - let bLen = b.length; + const aLen = a.length; + const bLen = b.length; if (aLen !== bLen) { return false; } @@ -49,8 +50,8 @@ export class LineDecoration { let result: LineDecoration[] = [], resultLen = 0; for (let i = 0, len = lineDecorations.length; i < len; i++) { - let d = lineDecorations[i]; - let range = d.range; + const d = lineDecorations[i]; + const range = d.range; if (range.endLineNumber < lineNumber || range.startLineNumber > lineNumber) { // Ignore decorations that sit outside this line @@ -62,8 +63,8 @@ export class LineDecoration { continue; } - let startColumn = (range.startLineNumber === lineNumber ? range.startColumn : minLineColumn); - let endColumn = (range.endLineNumber === lineNumber ? range.endColumn : maxLineColumn); + const startColumn = (range.startLineNumber === lineNumber ? range.startColumn : minLineColumn); + const endColumn = (range.endLineNumber === lineNumber ? range.endColumn : maxLineColumn); result[resultLen++] = new LineDecoration(startColumn, endColumn, d.inlineClassName, d.type); } @@ -71,16 +72,25 @@ export class LineDecoration { return result; } + private static _typeCompare(a: InlineDecorationType, b: InlineDecorationType): number { + const ORDER = [2, 0, 1, 3]; + return ORDER[a] - ORDER[b]; + } + public static compare(a: LineDecoration, b: LineDecoration): number { if (a.startColumn === b.startColumn) { if (a.endColumn === b.endColumn) { - if (a.className < b.className) { - return -1; + const typeCmp = LineDecoration._typeCompare(a.type, b.type); + if (typeCmp === 0) { + if (a.className < b.className) { + return -1; + } + if (a.className > b.className) { + return 1; + } + return 0; } - if (a.className > b.className) { - return 1; - } - return 0; + return typeCmp; } return a.endColumn - b.endColumn; } @@ -92,11 +102,13 @@ export class DecorationSegment { startOffset: number; endOffset: number; className: string; + metadata: number; - constructor(startOffset: number, endOffset: number, className: string) { + constructor(startOffset: number, endOffset: number, className: string, metadata: number) { this.startOffset = startOffset; this.endOffset = endOffset; this.className = className; + this.metadata = metadata; } } @@ -104,13 +116,23 @@ class Stack { public count: number; private readonly stopOffsets: number[]; private readonly classNames: string[]; + private readonly metadata: number[]; constructor() { this.stopOffsets = []; this.classNames = []; + this.metadata = []; this.count = 0; } + private static _metadata(metadata: number[]): number { + let result = 0; + for (let i = 0, len = metadata.length; i < len; i++) { + result |= metadata[i]; + } + return result; + } + public consumeLowerThan(maxStopOffset: number, nextStartOffset: number, result: DecorationSegment[]): number { while (this.count > 0 && this.stopOffsets[0] < maxStopOffset) { @@ -122,34 +144,37 @@ class Stack { } // Basically we are consuming the first i + 1 elements of the stack - result.push(new DecorationSegment(nextStartOffset, this.stopOffsets[i], this.classNames.join(' '))); + result.push(new DecorationSegment(nextStartOffset, this.stopOffsets[i], this.classNames.join(' '), Stack._metadata(this.metadata))); nextStartOffset = this.stopOffsets[i] + 1; // Consume them this.stopOffsets.splice(0, i + 1); this.classNames.splice(0, i + 1); + this.metadata.splice(0, i + 1); this.count -= (i + 1); } if (this.count > 0 && nextStartOffset < maxStopOffset) { - result.push(new DecorationSegment(nextStartOffset, maxStopOffset - 1, this.classNames.join(' '))); + result.push(new DecorationSegment(nextStartOffset, maxStopOffset - 1, this.classNames.join(' '), Stack._metadata(this.metadata))); nextStartOffset = maxStopOffset; } return nextStartOffset; } - public insert(stopOffset: number, className: string): void { + public insert(stopOffset: number, className: string, metadata: number): void { if (this.count === 0 || this.stopOffsets[this.count - 1] <= stopOffset) { // Insert at the end this.stopOffsets.push(stopOffset); this.classNames.push(className); + this.metadata.push(metadata); } else { // Find the insertion position for `stopOffset` for (let i = 0; i < this.count; i++) { if (this.stopOffsets[i] >= stopOffset) { this.stopOffsets.splice(i, 0, stopOffset); this.classNames.splice(i, 0, className); + this.metadata.splice(i, 0, metadata); break; } } @@ -170,14 +195,21 @@ export class LineDecorationsNormalizer { let result: DecorationSegment[] = []; - let stack = new Stack(); + const stack = new Stack(); let nextStartOffset = 0; for (let i = 0, len = lineDecorations.length; i < len; i++) { - let d = lineDecorations[i]; + const d = lineDecorations[i]; let startColumn = d.startColumn; let endColumn = d.endColumn; - let className = d.className; + const className = d.className; + const metadata = ( + d.type === InlineDecorationType.Before + ? LinePartMetadata.PSEUDO_BEFORE + : d.type === InlineDecorationType.After + ? LinePartMetadata.PSEUDO_AFTER + : 0 + ); // If the position would end up in the middle of a high-low surrogate pair, we move it to before the pair if (startColumn > 1) { @@ -194,15 +226,15 @@ export class LineDecorationsNormalizer { } } - let currentStartOffset = startColumn - 1; - let currentEndOffset = endColumn - 2; + const currentStartOffset = startColumn - 1; + const currentEndOffset = endColumn - 2; nextStartOffset = stack.consumeLowerThan(currentStartOffset, nextStartOffset, result); if (stack.count === 0) { nextStartOffset = currentStartOffset; } - stack.insert(currentEndOffset, className); + stack.insert(currentEndOffset, className, metadata); } stack.consumeLowerThan(Constants.MAX_SAFE_SMALL_INTEGER, nextStartOffset, result); diff --git a/src/vs/editor/common/viewLayout/viewLineRenderer.ts b/src/vs/editor/common/viewLayout/viewLineRenderer.ts index 05e628ab095..c032724b8ba 100644 --- a/src/vs/editor/common/viewLayout/viewLineRenderer.ts +++ b/src/vs/editor/common/viewLayout/viewLineRenderer.ts @@ -17,6 +17,16 @@ export const enum RenderWhitespace { All = 3 } +export const enum LinePartMetadata { + IS_WHITESPACE = 1, + PSEUDO_BEFORE = 2, + PSEUDO_AFTER = 4, + + IS_WHITESPACE_MASK = 0b001, + PSEUDO_BEFORE_MASK = 0b010, + PSEUDO_AFTER_MASK = 0b100, +} + class LinePart { _linePartBrand: void; @@ -25,10 +35,16 @@ class LinePart { */ public readonly endIndex: number; public readonly type: string; + public readonly metadata: number; - constructor(endIndex: number, type: string) { + constructor(endIndex: number, type: string, metadata: number) { this.endIndex = endIndex; this.type = type; + this.metadata = metadata; + } + + public isWhitespace(): boolean { + return (this.metadata & LinePartMetadata.IS_WHITESPACE_MASK ? true : false); } } @@ -470,7 +486,7 @@ function transformAndRemoveOverflowing(tokens: IViewLineTokens, fauxIndentLength // The faux indent part of the line should have no token type if (fauxIndentLength > 0) { - result[resultLen++] = new LinePart(fauxIndentLength, ''); + result[resultLen++] = new LinePart(fauxIndentLength, '', 0); } for (let tokenIndex = 0, tokensLen = tokens.getCount(); tokenIndex < tokensLen; tokenIndex++) { @@ -481,10 +497,10 @@ function transformAndRemoveOverflowing(tokens: IViewLineTokens, fauxIndentLength } const type = tokens.getClassName(tokenIndex); if (endIndex >= len) { - result[resultLen++] = new LinePart(len, type); + result[resultLen++] = new LinePart(len, type, 0); break; } - result[resultLen++] = new LinePart(endIndex, type); + result[resultLen++] = new LinePart(endIndex, type, 0); } return result; @@ -513,6 +529,7 @@ function splitLargeTokens(lineContent: string, tokens: LinePart[], onlyAtSpaces: const tokenEndIndex = token.endIndex; if (lastTokenEndIndex + Constants.LongToken < tokenEndIndex) { const tokenType = token.type; + const tokenMetadata = token.metadata; let lastSpaceOffset = -1; let currTokenStart = lastTokenEndIndex; @@ -522,13 +539,13 @@ function splitLargeTokens(lineContent: string, tokens: LinePart[], onlyAtSpaces: } if (lastSpaceOffset !== -1 && j - currTokenStart >= Constants.LongToken) { // Split at `lastSpaceOffset` + 1 - result[resultLen++] = new LinePart(lastSpaceOffset + 1, tokenType); + result[resultLen++] = new LinePart(lastSpaceOffset + 1, tokenType, tokenMetadata); currTokenStart = lastSpaceOffset + 1; lastSpaceOffset = -1; } } if (currTokenStart !== tokenEndIndex) { - result[resultLen++] = new LinePart(tokenEndIndex, tokenType); + result[resultLen++] = new LinePart(tokenEndIndex, tokenType, tokenMetadata); } } else { result[resultLen++] = token; @@ -544,12 +561,13 @@ function splitLargeTokens(lineContent: string, tokens: LinePart[], onlyAtSpaces: let diff = (tokenEndIndex - lastTokenEndIndex); if (diff > Constants.LongToken) { const tokenType = token.type; + const tokenMetadata = token.metadata; const piecesCount = Math.ceil(diff / Constants.LongToken); for (let j = 1; j < piecesCount; j++) { let pieceEndIndex = lastTokenEndIndex + (j * Constants.LongToken); - result[resultLen++] = new LinePart(pieceEndIndex, tokenType); + result[resultLen++] = new LinePart(pieceEndIndex, tokenType, tokenMetadata); } - result[resultLen++] = new LinePart(tokenEndIndex, tokenType); + result[resultLen++] = new LinePart(tokenEndIndex, tokenType, tokenMetadata); } else { result[resultLen++] = token; } @@ -640,17 +658,17 @@ function _applyRenderWhitespace(input: RenderLineInput, lineContent: string, len if (generateLinePartForEachWhitespace) { const lastEndIndex = (resultLen > 0 ? result[resultLen - 1].endIndex : fauxIndentLength); for (let i = lastEndIndex + 1; i <= charIndex; i++) { - result[resultLen++] = new LinePart(i, 'mtkw'); + result[resultLen++] = new LinePart(i, 'mtkw', LinePartMetadata.IS_WHITESPACE); } } else { - result[resultLen++] = new LinePart(charIndex, 'mtkw'); + result[resultLen++] = new LinePart(charIndex, 'mtkw', LinePartMetadata.IS_WHITESPACE); } tmpIndent = tmpIndent % tabSize; } } else { // was in regular token if (charIndex === tokenEndIndex || (isInWhitespace && charIndex > fauxIndentLength)) { - result[resultLen++] = new LinePart(charIndex, tokenType); + result[resultLen++] = new LinePart(charIndex, tokenType, 0); tmpIndent = tmpIndent % tabSize; } } @@ -693,13 +711,13 @@ function _applyRenderWhitespace(input: RenderLineInput, lineContent: string, len if (generateLinePartForEachWhitespace) { const lastEndIndex = (resultLen > 0 ? result[resultLen - 1].endIndex : fauxIndentLength); for (let i = lastEndIndex + 1; i <= len; i++) { - result[resultLen++] = new LinePart(i, 'mtkw'); + result[resultLen++] = new LinePart(i, 'mtkw', LinePartMetadata.IS_WHITESPACE); } } else { - result[resultLen++] = new LinePart(len, 'mtkw'); + result[resultLen++] = new LinePart(len, 'mtkw', LinePartMetadata.IS_WHITESPACE); } } else { - result[resultLen++] = new LinePart(len, tokenType); + result[resultLen++] = new LinePart(len, tokenType, 0); } return result; @@ -720,42 +738,45 @@ function _applyInlineDecorations(lineContent: string, len: number, tokens: LineP const token = tokens[tokenIndex]; const tokenEndIndex = token.endIndex; const tokenType = token.type; + const tokenMetadata = token.metadata; while (lineDecorationIndex < lineDecorationsLen && lineDecorations[lineDecorationIndex].startOffset < tokenEndIndex) { const lineDecoration = lineDecorations[lineDecorationIndex]; if (lineDecoration.startOffset > lastResultEndIndex) { lastResultEndIndex = lineDecoration.startOffset; - result[resultLen++] = new LinePart(lastResultEndIndex, tokenType); + result[resultLen++] = new LinePart(lastResultEndIndex, tokenType, tokenMetadata); } if (lineDecoration.endOffset + 1 <= tokenEndIndex) { // This line decoration ends before this token ends lastResultEndIndex = lineDecoration.endOffset + 1; - result[resultLen++] = new LinePart(lastResultEndIndex, tokenType + ' ' + lineDecoration.className); + result[resultLen++] = new LinePart(lastResultEndIndex, tokenType + ' ' + lineDecoration.className, tokenMetadata | lineDecoration.metadata); lineDecorationIndex++; } else { // This line decoration continues on to the next token lastResultEndIndex = tokenEndIndex; - result[resultLen++] = new LinePart(lastResultEndIndex, tokenType + ' ' + lineDecoration.className); + result[resultLen++] = new LinePart(lastResultEndIndex, tokenType + ' ' + lineDecoration.className, tokenMetadata | lineDecoration.metadata); break; } } if (tokenEndIndex > lastResultEndIndex) { lastResultEndIndex = tokenEndIndex; - result[resultLen++] = new LinePart(lastResultEndIndex, tokenType); + result[resultLen++] = new LinePart(lastResultEndIndex, tokenType, tokenMetadata); } } const lastTokenEndIndex = tokens[tokens.length - 1].endIndex; if (lineDecorationIndex < lineDecorationsLen && lineDecorations[lineDecorationIndex].startOffset === lastTokenEndIndex) { let classNames: string[] = []; + let metadata = 0; while (lineDecorationIndex < lineDecorationsLen && lineDecorations[lineDecorationIndex].startOffset === lastTokenEndIndex) { classNames.push(lineDecorations[lineDecorationIndex].className); + metadata |= lineDecorations[lineDecorationIndex].metadata; lineDecorationIndex++; } - result[resultLen++] = new LinePart(lastResultEndIndex, classNames.join(' ')); + result[resultLen++] = new LinePart(lastResultEndIndex, classNames.join(' '), metadata); } return result; @@ -788,6 +809,7 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render let visibleColumn = startVisibleColumn; let charOffsetInPart = 0; + let partDisplacement = 0; let prevPartContentCnt = 0; let partAbsoluteOffset = 0; @@ -799,8 +821,9 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render const part = parts[partIndex]; const partEndIndex = part.endIndex; const partType = part.type; - const partRendersWhitespace = (renderWhitespace !== RenderWhitespace.None && (partType.indexOf('mtkw') >= 0)); + const partRendersWhitespace = (renderWhitespace !== RenderWhitespace.None && part.isWhitespace()); const partRendersWhitespaceWithWidth = partRendersWhitespace && !fontIsMonospace && (partType === 'mtkw'/*only whitespace*/ || !containsForeignElements); + const partIsEmptyAndHasPseudoAfter = (charIndex === partEndIndex && part.metadata === LinePartMetadata.PSEUDO_AFTER); charOffsetInPart = 0; sb.appendASCIIString('= 1)) { + const lineContent = model.getLineContent(marker.startLineNumber); + ariaLabel = `${lineContent}, ${ariaLabel}`; + } + return ariaLabel; + } } export class MarkerNavigationWidget extends PeekViewWidget { @@ -316,7 +344,7 @@ export class MarkerNavigationWidget extends PeekViewWidget { } this._icon.className = `codicon ${SeverityIcon.className(MarkerSeverity.toSeverity(this._severity))}`; - this.editor.revealPositionInCenter(position, ScrollType.Smooth); + this.editor.revealPositionNearTop(position, ScrollType.Smooth); this.editor.focus(); } diff --git a/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts b/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts index 7c279e25546..dc6368509b3 100644 --- a/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts +++ b/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts @@ -33,6 +33,7 @@ import * as peekView from 'vs/editor/contrib/peekView/peekView'; import { FileReferences, OneReference, ReferencesModel } from '../referencesModel'; import { FuzzyScore } from 'vs/base/common/filters'; import { SplitView, Sizing } from 'vs/base/browser/ui/splitview/splitview'; +import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; class DecorationsManager implements IDisposable { @@ -215,7 +216,8 @@ export class ReferenceWidget extends peekView.PeekViewWidget { @ITextModelService private readonly _textModelResolverService: ITextModelService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @peekView.IPeekViewService private readonly _peekViewService: peekView.IPeekViewService, - @ILabelService private readonly _uriLabel: ILabelService + @ILabelService private readonly _uriLabel: ILabelService, + @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, ) { super(editor, { showFrame: false, showArrow: true, isResizeable: true, isAccessible: true }); @@ -304,7 +306,7 @@ export class ReferenceWidget extends peekView.PeekViewWidget { }; this._preview = this._instantiationService.createInstance(EmbeddedCodeEditorWidget, this._previewContainer, options, this.editor); dom.hide(this._previewContainer); - this._previewNotAvailableMessage = TextModel.createFromString(nls.localize('missingPreviewMessage', "no preview available")); + this._previewNotAvailableMessage = new TextModel(nls.localize('missingPreviewMessage', "no preview available"), TextModel.DEFAULT_CREATION_OPTIONS, null, null, this._undoRedoService); // tree this._treeContainer = dom.append(containerElement, dom.$('div.ref-tree.inline')); diff --git a/src/vs/editor/contrib/hover/modesContentHover.ts b/src/vs/editor/contrib/hover/modesContentHover.ts index 3e41cf7b901..3f03ee3e821 100644 --- a/src/vs/editor/contrib/hover/modesContentHover.ts +++ b/src/vs/editor/contrib/hover/modesContentHover.ts @@ -40,6 +40,7 @@ import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Constants } from 'vs/base/common/uint'; import { textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; +import { Progress } from 'vs/platform/progress/common/progress'; const $ = dom.$; @@ -626,6 +627,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { this._editor.getModel()!, new Range(marker.startLineNumber, marker.startColumn, marker.endLineNumber, marker.endColumn), markerCodeActionTrigger, + Progress.None, cancellationToken); }); } diff --git a/src/vs/editor/contrib/links/getLinks.ts b/src/vs/editor/contrib/links/getLinks.ts index a85e7f8c942..0ee40d5166e 100644 --- a/src/vs/editor/contrib/links/getLinks.ts +++ b/src/vs/editor/contrib/links/getLinks.ts @@ -77,8 +77,8 @@ export class LinksList extends Disposable { const newLinks = list.links.map(link => new Link(link, provider)); links = LinksList._union(links, newLinks); // register disposables - if (isDisposable(provider)) { - this._register(provider); + if (isDisposable(list)) { + this._register(list); } } this.links = links; diff --git a/src/vs/editor/contrib/parameterHints/test/parameterHintsModel.test.ts b/src/vs/editor/contrib/parameterHints/test/parameterHintsModel.test.ts index 2aa301d692a..2d5e1967fe7 100644 --- a/src/vs/editor/contrib/parameterHints/test/parameterHintsModel.test.ts +++ b/src/vs/editor/contrib/parameterHints/test/parameterHintsModel.test.ts @@ -10,7 +10,7 @@ import { URI } from 'vs/base/common/uri'; import { Position } from 'vs/editor/common/core/position'; import { Handler } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; -import { TextModel } from 'vs/editor/common/model/textModel'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import * as modes from 'vs/editor/common/modes'; import { createTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; @@ -49,7 +49,7 @@ suite('ParameterHintsModel', () => { }); function createMockEditor(fileContents: string) { - const textModel = TextModel.createFromString(fileContents, undefined, undefined, mockFile); + const textModel = createTextModel(fileContents, undefined, undefined, mockFile); const editor = createTestCodeEditor({ model: textModel, serviceCollection: new ServiceCollection( diff --git a/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts b/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts index 4ebcba775cc..e56cb2bb72f 100644 --- a/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts +++ b/src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts @@ -20,6 +20,8 @@ import { TestTextResourcePropertiesService } from 'vs/editor/test/common/service import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { NullLogService } from 'vs/platform/log/common/log'; import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; +import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; class MockJSMode extends MockMode { @@ -48,7 +50,7 @@ suite('SmartSelect', () => { setup(() => { const configurationService = new TestConfigurationService(); - modelService = new ModelServiceImpl(configurationService, new TestTextResourcePropertiesService(configurationService), new TestThemeService(), new NullLogService(), new UndoRedoService()); + modelService = new ModelServiceImpl(configurationService, new TestTextResourcePropertiesService(configurationService), new TestThemeService(), new NullLogService(), new UndoRedoService(new TestDialogService(), new TestNotificationService())); mode = new MockJSMode(); }); diff --git a/src/vs/editor/contrib/snippet/test/snippetController2.test.ts b/src/vs/editor/contrib/snippet/test/snippetController2.test.ts index 29703e9307a..4f67e00ac4e 100644 --- a/src/vs/editor/contrib/snippet/test/snippetController2.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetController2.test.ts @@ -12,6 +12,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { NullLogService } from 'vs/platform/log/common/log'; import { Handler } from 'vs/editor/common/editorCommon'; import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; suite('SnippetController2', function () { @@ -36,7 +37,7 @@ suite('SnippetController2', function () { setup(function () { contextKeys = new MockContextKeyService(); - model = TextModel.createFromString('if\n $state\nfi'); + model = createTextModel('if\n $state\nfi'); editor = createTestCodeEditor({ model: model }); editor.setSelections([new Selection(1, 1, 1, 1), new Selection(2, 5, 2, 5)]); assert.equal(model.getEOL(), '\n'); diff --git a/src/vs/editor/contrib/snippet/test/snippetSession.test.ts b/src/vs/editor/contrib/snippet/test/snippetSession.test.ts index d4ae07a4a49..cedf6be949e 100644 --- a/src/vs/editor/contrib/snippet/test/snippetSession.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetSession.test.ts @@ -11,6 +11,7 @@ import { TextModel } from 'vs/editor/common/model/textModel'; import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser'; import { SnippetSession } from 'vs/editor/contrib/snippet/snippetSession'; import { createTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; suite('SnippetSession', function () { @@ -26,7 +27,7 @@ suite('SnippetSession', function () { } setup(function () { - model = TextModel.createFromString('function foo() {\n console.log(a);\n}'); + model = createTextModel('function foo() {\n console.log(a);\n}'); editor = createTestCodeEditor({ model: model }) as IActiveCodeEditor; editor.setSelections([new Selection(1, 1, 1, 1), new Selection(2, 5, 2, 5)]); assert.equal(model.getEOL(), '\n'); diff --git a/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts b/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts index 06eb5f8777c..b7012bec477 100644 --- a/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts @@ -12,6 +12,7 @@ import { TextModel } from 'vs/editor/common/model/textModel'; import { Workspace, toWorkspaceFolders, IWorkspace, IWorkspaceContextService, toWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ILabelService } from 'vs/platform/label/common/label'; import { mock } from 'vs/editor/contrib/suggest/test/suggestModel.test'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; suite('Snippet Variables Resolver', function () { @@ -25,7 +26,7 @@ suite('Snippet Variables Resolver', function () { let resolver: VariableResolver; setup(function () { - model = TextModel.createFromString([ + model = createTextModel([ 'this is line one', 'this is line two', ' this is line three' @@ -67,7 +68,7 @@ suite('Snippet Variables Resolver', function () { resolver = new ModelBasedVariableResolver( labelService, - TextModel.createFromString('', undefined, undefined, URI.parse('http://www.pb.o/abc/def/ghi')) + createTextModel('', undefined, undefined, URI.parse('http://www.pb.o/abc/def/ghi')) ); assertVariableResolve(resolver, 'TM_FILENAME', 'ghi'); if (!isWindows) { @@ -77,7 +78,7 @@ suite('Snippet Variables Resolver', function () { resolver = new ModelBasedVariableResolver( labelService, - TextModel.createFromString('', undefined, undefined, URI.parse('mem:fff.ts')) + createTextModel('', undefined, undefined, URI.parse('mem:fff.ts')) ); assertVariableResolve(resolver, 'TM_DIRECTORY', ''); assertVariableResolve(resolver, 'TM_FILEPATH', 'fff.ts'); @@ -92,7 +93,7 @@ suite('Snippet Variables Resolver', function () { } }; - const model = TextModel.createFromString([].join('\n'), undefined, undefined, URI.parse('foo:///foo/files/text.txt')); + const model = createTextModel([].join('\n'), undefined, undefined, URI.parse('foo:///foo/files/text.txt')); const resolver = new CompositeSnippetVariableResolver([new ModelBasedVariableResolver(labelService, model)]); @@ -144,19 +145,19 @@ suite('Snippet Variables Resolver', function () { resolver = new ModelBasedVariableResolver( labelService, - TextModel.createFromString('', undefined, undefined, URI.parse('http://www.pb.o/abc/def/ghi')) + createTextModel('', undefined, undefined, URI.parse('http://www.pb.o/abc/def/ghi')) ); assertVariableResolve(resolver, 'TM_FILENAME_BASE', 'ghi'); resolver = new ModelBasedVariableResolver( labelService, - TextModel.createFromString('', undefined, undefined, URI.parse('mem:.git')) + createTextModel('', undefined, undefined, URI.parse('mem:.git')) ); assertVariableResolve(resolver, 'TM_FILENAME_BASE', '.git'); resolver = new ModelBasedVariableResolver( labelService, - TextModel.createFromString('', undefined, undefined, URI.parse('mem:foo.')) + createTextModel('', undefined, undefined, URI.parse('mem:foo.')) ); assertVariableResolve(resolver, 'TM_FILENAME_BASE', 'foo'); }); diff --git a/src/vs/editor/contrib/suggest/test/suggest.test.ts b/src/vs/editor/contrib/suggest/test/suggest.test.ts index da210d07ab2..f22d8898a0f 100644 --- a/src/vs/editor/contrib/suggest/test/suggest.test.ts +++ b/src/vs/editor/contrib/suggest/test/suggest.test.ts @@ -10,6 +10,7 @@ import { provideSuggestionItems, SnippetSortOrder, CompletionOptions } from 'vs/ import { Position } from 'vs/editor/common/core/position'; import { TextModel } from 'vs/editor/common/model/textModel'; import { Range } from 'vs/editor/common/core/range'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; suite('Suggest', function () { @@ -19,7 +20,7 @@ suite('Suggest', function () { setup(function () { - model = TextModel.createFromString('FOO\nbar\BAR\nfoo', undefined, undefined, URI.parse('foo:bar/path')); + model = createTextModel('FOO\nbar\BAR\nfoo', undefined, undefined, URI.parse('foo:bar/path')); registration = CompletionProviderRegistry.register({ pattern: 'bar/path', scheme: 'foo' }, { provideCompletionItems(_doc, pos) { return { diff --git a/src/vs/editor/contrib/suggest/test/suggestController.test.ts b/src/vs/editor/contrib/suggest/test/suggestController.test.ts index b94da857b16..06c95543ec3 100644 --- a/src/vs/editor/contrib/suggest/test/suggestController.test.ts +++ b/src/vs/editor/contrib/suggest/test/suggestController.test.ts @@ -23,6 +23,7 @@ import { CompletionProviderRegistry, CompletionItemKind, CompletionItemInsertTex import { Event } from 'vs/base/common/event'; import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; import { IMenuService, IMenu } from 'vs/platform/actions/common/actions'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; suite('SuggestController', function () { @@ -57,7 +58,7 @@ suite('SuggestController', function () { }] ); - model = TextModel.createFromString('', undefined, undefined, URI.from({ scheme: 'test-ctrl', path: '/path.tst' })); + model = createTextModel('', undefined, undefined, URI.from({ scheme: 'test-ctrl', path: '/path.tst' })); editor = createTestCodeEditor({ model, serviceCollection, diff --git a/src/vs/editor/contrib/suggest/test/suggestMemory.test.ts b/src/vs/editor/contrib/suggest/test/suggestMemory.test.ts index fd2ee8ec49c..28deea319ab 100644 --- a/src/vs/editor/contrib/suggest/test/suggestMemory.test.ts +++ b/src/vs/editor/contrib/suggest/test/suggestMemory.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { LRUMemory, NoMemory, PrefixMemory, Memory } from 'vs/editor/contrib/suggest/suggestMemory'; import { ITextModel } from 'vs/editor/common/model'; -import { TextModel } from 'vs/editor/common/model/textModel'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { createSuggestItem } from 'vs/editor/contrib/suggest/test/completionModel.test'; import { IPosition } from 'vs/editor/common/core/position'; import { CompletionItem } from 'vs/editor/contrib/suggest/suggest'; @@ -19,7 +19,7 @@ suite('SuggestMemories', function () { setup(function () { pos = { lineNumber: 1, column: 1 }; - buffer = TextModel.createFromString('This is some text.\nthis.\nfoo: ,'); + buffer = createTextModel('This is some text.\nthis.\nfoo: ,'); items = [ createSuggestItem('foo', 0), createSuggestItem('bar', 0) diff --git a/src/vs/editor/contrib/suggest/test/suggestModel.test.ts b/src/vs/editor/contrib/suggest/test/suggestModel.test.ts index c7e14186f2e..c0c0a86d776 100644 --- a/src/vs/editor/contrib/suggest/test/suggestModel.test.ts +++ b/src/vs/editor/contrib/suggest/test/suggestModel.test.ts @@ -32,6 +32,7 @@ import { ISuggestMemoryService } from 'vs/editor/contrib/suggest/suggestMemory'; import { ITextModel } from 'vs/editor/common/model'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; export interface Ctor { new(): T; @@ -124,7 +125,7 @@ suite('SuggestModel - Context', function () { }); test('Context - shouldAutoTrigger', function () { - const model = TextModel.createFromString('Das Pferd frisst keinen Gurkensalat - Philipp Reis 1861.\nWer hat\'s erfunden?'); + const model = createTextModel('Das Pferd frisst keinen Gurkensalat - Philipp Reis 1861.\nWer hat\'s erfunden?'); disposables.push(model); assertAutoTrigger(model, 3, true, 'end of word, Das|'); @@ -138,7 +139,7 @@ suite('SuggestModel - Context', function () { const innerMode = new InnerMode(); disposables.push(outerMode, innerMode); - const model = TextModel.createFromString('aa', undefined, outerMode.getLanguageIdentifier()); + const model = createTextModel('aa', undefined, outerMode.getLanguageIdentifier()); disposables.push(model); assertAutoTrigger(model, 1, true, 'a| new NullLogService()); - export const undoRedoService = define(IUndoRedoService, () => new UndoRedoService()); + export const undoRedoService = define(IUndoRedoService, (o) => new UndoRedoService(dialogService.get(o), notificationService.get(o))); export const modelService = define(IModelService, (o) => new ModelServiceImpl(configurationService.get(o), resourcePropertiesService.get(o), standaloneThemeService.get(o), logService.get(o), undoRedoService.get(o))); diff --git a/src/vs/editor/test/browser/commands/sideEditing.test.ts b/src/vs/editor/test/browser/commands/sideEditing.test.ts index 540771d0c72..49514eef350 100644 --- a/src/vs/editor/test/browser/commands/sideEditing.test.ts +++ b/src/vs/editor/test/browser/commands/sideEditing.test.ts @@ -10,7 +10,7 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; -import { TextModel } from 'vs/editor/common/model/textModel'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { TestConfiguration } from 'vs/editor/test/common/mocks/testConfiguration'; @@ -199,7 +199,7 @@ suite('SideEditing', () => { ]; function _runTest(selection: Selection, editRange: Range, editText: string, editForceMoveMarkers: boolean, expected: Selection, msg: string): void { - const model = TextModel.createFromString(LINES.join('\n')); + const model = createTextModel(LINES.join('\n')); const config = new TestConfiguration({}); const monospaceLineBreaksComputerFactory = MonospaceLineBreaksComputerFactory.create(config.options); const viewModel = new ViewModel(0, config, model, monospaceLineBreaksComputerFactory, monospaceLineBreaksComputerFactory, null!); diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index fa60d5989ec..b6984401643 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -5551,4 +5551,28 @@ suite('Undo stops', () => { }); }); + test('can undo typing and EOL change in one undo stop', () => { + let model = createTextModel( + [ + 'A line', + 'Another line', + ].join('\n') + ); + + withTestCodeEditor(null, { model: model }, (editor, cursor) => { + cursor.setSelections('test', [new Selection(1, 3, 1, 3)]); + cursorCommand(cursor, H.Type, { text: 'first' }, 'keyboard'); + assert.equal(model.getValue(), 'A first line\nAnother line'); + assertCursor(cursor, new Selection(1, 8, 1, 8)); + + model.pushEOL(EndOfLineSequence.CRLF); + assert.equal(model.getValue(), 'A first line\r\nAnother line'); + assertCursor(cursor, new Selection(1, 8, 1, 8)); + + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + assert.equal(model.getValue(), 'A line\nAnother line'); + assertCursor(cursor, new Selection(1, 3, 1, 3)); + }); + }); + }); diff --git a/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts b/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts index 36f2f54dc23..e918feacef0 100644 --- a/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts +++ b/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts @@ -14,6 +14,7 @@ import { TextModel } from 'vs/editor/common/model/textModel'; import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; import { TestConfiguration } from 'vs/editor/test/common/mocks/testConfiguration'; import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/monospaceLineBreaksComputer'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; suite('Cursor move command test', () => { @@ -31,7 +32,7 @@ suite('Cursor move command test', () => { '1' ].join('\n'); - thisModel = TextModel.createFromString(text); + thisModel = createTextModel(text); thisConfiguration = new TestConfiguration({}); const monospaceLineBreaksComputerFactory = MonospaceLineBreaksComputerFactory.create(thisConfiguration.options); thisViewModel = new ViewModel(0, thisConfiguration, thisModel, monospaceLineBreaksComputerFactory, monospaceLineBreaksComputerFactory, null!); diff --git a/src/vs/editor/test/browser/controller/textAreaState.test.ts b/src/vs/editor/test/browser/controller/textAreaState.test.ts index 005a30b465f..163e9de5422 100644 --- a/src/vs/editor/test/browser/controller/textAreaState.test.ts +++ b/src/vs/editor/test/browser/controller/textAreaState.test.ts @@ -8,7 +8,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { ITextAreaWrapper, PagedScreenReaderStrategy, TextAreaState } from 'vs/editor/browser/controller/textAreaState'; import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; -import { TextModel } from 'vs/editor/common/model/textModel'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; export class MockTextAreaWrapper extends Disposable implements ITextAreaWrapper { @@ -506,7 +506,7 @@ suite('TextAreaState', () => { suite('PagedScreenReaderStrategy', () => { function testPagedScreenReaderStrategy(lines: string[], selection: Selection, expected: TextAreaState): void { - const model = TextModel.createFromString(lines.join('\n')); + const model = createTextModel(lines.join('\n')); const actual = PagedScreenReaderStrategy.fromEditorSelection(TextAreaState.EMPTY, model, selection, 10, true); assert.ok(equalsTextAreaState(actual, expected)); model.dispose(); diff --git a/src/vs/editor/test/browser/testCodeEditor.ts b/src/vs/editor/test/browser/testCodeEditor.ts index d138d2e396a..5153fc082ec 100644 --- a/src/vs/editor/test/browser/testCodeEditor.ts +++ b/src/vs/editor/test/browser/testCodeEditor.ts @@ -11,7 +11,7 @@ import * as editorOptions from 'vs/editor/common/config/editorOptions'; import { Cursor } from 'vs/editor/common/controller/cursor'; import { IConfiguration, IEditorContribution } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; -import { TextModel } from 'vs/editor/common/model/textModel'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; import { TestCodeEditorService, TestCommandService } from 'vs/editor/test/browser/editorTestServices'; import { TestConfiguration } from 'vs/editor/test/common/mocks/testConfiguration'; @@ -77,9 +77,9 @@ export function withTestCodeEditor(text: string | string[] | null, options: Test // create a model if necessary and remember it in order to dispose it. if (!options.model) { if (typeof text === 'string') { - options.model = TextModel.createFromString(text); + options.model = createTextModel(text); } else if (text) { - options.model = TextModel.createFromString(text.join('\n')); + options.model = createTextModel(text.join('\n')); } } diff --git a/src/vs/editor/test/browser/testCommand.ts b/src/vs/editor/test/browser/testCommand.ts index f5670377c6e..4c280c1d960 100644 --- a/src/vs/editor/test/browser/testCommand.ts +++ b/src/vs/editor/test/browser/testCommand.ts @@ -8,7 +8,7 @@ import { IRange } from 'vs/editor/common/core/range'; import { Selection, ISelection } from 'vs/editor/common/core/selection'; import { ICommand, Handler, IEditOperationBuilder } from 'vs/editor/common/editorCommon'; import { IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model'; -import { TextModel } from 'vs/editor/common/model/textModel'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { LanguageIdentifier } from 'vs/editor/common/modes'; import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; @@ -21,7 +21,7 @@ export function testCommand( expectedSelection: Selection, forceTokenization?: boolean ): void { - let model = TextModel.createFromString(lines.join('\n'), undefined, languageIdentifier); + let model = createTextModel(lines.join('\n'), undefined, languageIdentifier); withTestCodeEditor('', { model: model }, (_editor, cursor) => { if (!cursor) { return; diff --git a/src/vs/editor/test/common/editorTestUtils.ts b/src/vs/editor/test/common/editorTestUtils.ts index 3116fa31171..8298cf24b81 100644 --- a/src/vs/editor/test/common/editorTestUtils.ts +++ b/src/vs/editor/test/common/editorTestUtils.ts @@ -7,9 +7,12 @@ import { URI } from 'vs/base/common/uri'; import { DefaultEndOfLine, ITextModelCreationOptions } from 'vs/editor/common/model'; import { TextModel } from 'vs/editor/common/model/textModel'; import { LanguageIdentifier } from 'vs/editor/common/modes'; +import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; +import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; export function withEditorModel(text: string[], callback: (model: TextModel) => void): void { - let model = TextModel.createFromString(text.join('\n')); + let model = createTextModel(text.join('\n')); callback(model); model.dispose(); } @@ -36,5 +39,8 @@ export function createTextModel(text: string, _options: IRelaxedTextModelCreatio isForSimpleWidget: (typeof _options.isForSimpleWidget === 'undefined' ? TextModel.DEFAULT_CREATION_OPTIONS.isForSimpleWidget : _options.isForSimpleWidget), largeFileOptimizations: (typeof _options.largeFileOptimizations === 'undefined' ? TextModel.DEFAULT_CREATION_OPTIONS.largeFileOptimizations : _options.largeFileOptimizations), }; - return TextModel.createFromString(text, options, languageIdentifier, uri); + const dialogService = new TestDialogService(); + const notificationService = new TestNotificationService(); + const undoRedoService = new UndoRedoService(dialogService, notificationService); + return new TextModel(text, options, languageIdentifier, uri, undoRedoService); } diff --git a/src/vs/editor/test/common/model/editableTextModel.test.ts b/src/vs/editor/test/common/model/editableTextModel.test.ts index bbb6b3dd781..b37a3b9820e 100644 --- a/src/vs/editor/test/common/model/editableTextModel.test.ts +++ b/src/vs/editor/test/common/model/editableTextModel.test.ts @@ -10,9 +10,10 @@ import { MirrorTextModel } from 'vs/editor/common/model/mirrorTextModel'; import { TextModel } from 'vs/editor/common/model/textModel'; import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; import { assertSyncedModels, testApplyEditsWithSyncedModels } from 'vs/editor/test/common/model/editableTextModelTestUtils'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; function createEditableTextModelFromString(text: string): TextModel { - return TextModel.createFromString(text, TextModel.DEFAULT_CREATION_OPTIONS, null); + return createTextModel(text, TextModel.DEFAULT_CREATION_OPTIONS, null); } suite('EditorModel - EditableTextModel.applyEdits updates mightContainRTL', () => { diff --git a/src/vs/editor/test/common/model/editableTextModelTestUtils.ts b/src/vs/editor/test/common/model/editableTextModelTestUtils.ts index a4124f4b519..e1e4b5c999e 100644 --- a/src/vs/editor/test/common/model/editableTextModelTestUtils.ts +++ b/src/vs/editor/test/common/model/editableTextModelTestUtils.ts @@ -9,6 +9,7 @@ import { EndOfLinePreference, EndOfLineSequence, IIdentifiedSingleEditOperation import { MirrorTextModel } from 'vs/editor/common/model/mirrorTextModel'; import { TextModel } from 'vs/editor/common/model/textModel'; import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; export function testApplyEditsWithSyncedModels(original: string[], edits: IIdentifiedSingleEditOperation[], expected: string[], inputEditsAreInvalid: boolean = false): void { let originalStr = original.join('\n'); @@ -88,7 +89,7 @@ function assertLineMapping(model: TextModel, msg: string): void { export function assertSyncedModels(text: string, callback: (model: TextModel, assertMirrorModels: () => void) => void, setup: ((model: TextModel) => void) | null = null): void { - let model = TextModel.createFromString(text, TextModel.DEFAULT_CREATION_OPTIONS, null); + let model = createTextModel(text, TextModel.DEFAULT_CREATION_OPTIONS, null); model.setEOL(EndOfLineSequence.LF); assertLineMapping(model, 'model'); diff --git a/src/vs/editor/test/common/model/model.line.test.ts b/src/vs/editor/test/common/model/model.line.test.ts index 518a30d94b8..139b169bf1d 100644 --- a/src/vs/editor/test/common/model/model.line.test.ts +++ b/src/vs/editor/test/common/model/model.line.test.ts @@ -9,6 +9,7 @@ import { Range } from 'vs/editor/common/core/range'; import { TextModel } from 'vs/editor/common/model/textModel'; import { LanguageIdentifier, MetadataConsts } from 'vs/editor/common/modes'; import { ViewLineToken, ViewLineTokenFactory } from 'vs/editor/test/common/core/viewLineToken'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; interface ILineEdit { startColumn: number; @@ -106,7 +107,7 @@ suite('ModelLinesTokens', () => { function testApplyEdits(initial: IBufferLineState[], edits: IEdit[], expected: IBufferLineState[]): void { const initialText = initial.map(el => el.text).join('\n'); - const model = TextModel.createFromString(initialText, TextModel.DEFAULT_CREATION_OPTIONS, new LanguageIdentifier('test', 0)); + const model = createTextModel(initialText, TextModel.DEFAULT_CREATION_OPTIONS, new LanguageIdentifier('test', 0)); for (let lineIndex = 0; lineIndex < initial.length; lineIndex++) { const lineTokens = initial[lineIndex].tokens; const lineTextLength = model.getLineMaxColumn(lineIndex + 1) - 1; @@ -442,7 +443,7 @@ suite('ModelLinesTokens', () => { } test('insertion on empty line', () => { - const model = TextModel.createFromString('some text', TextModel.DEFAULT_CREATION_OPTIONS, new LanguageIdentifier('test', 0)); + const model = createTextModel('some text', TextModel.DEFAULT_CREATION_OPTIONS, new LanguageIdentifier('test', 0)); const tokens = TestToken.toTokens([new TestToken(0, 1)]); LineTokens.convertToEndOffset(tokens, model.getLineMaxColumn(1) - 1); model.setLineTokens(1, tokens); diff --git a/src/vs/editor/test/common/model/model.modes.test.ts b/src/vs/editor/test/common/model/model.modes.test.ts index 559a6f89f07..ce313b77a70 100644 --- a/src/vs/editor/test/common/model/model.modes.test.ts +++ b/src/vs/editor/test/common/model/model.modes.test.ts @@ -12,6 +12,7 @@ import { TokenizationResult2 } from 'vs/editor/common/core/token'; import { TextModel } from 'vs/editor/common/model/textModel'; import * as modes from 'vs/editor/common/modes'; import { NULL_STATE } from 'vs/editor/common/modes/nullMode'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; // --------- utils @@ -46,7 +47,7 @@ suite('Editor Model - Model Modes 1', () => { const LANGUAGE_ID = 'modelModeTest1'; calledFor = []; languageRegistration = modes.TokenizationRegistry.register(LANGUAGE_ID, tokenizationSupport); - thisModel = TextModel.createFromString(TEXT, undefined, new modes.LanguageIdentifier(LANGUAGE_ID, 0)); + thisModel = createTextModel(TEXT, undefined, new modes.LanguageIdentifier(LANGUAGE_ID, 0)); }); teardown(() => { @@ -199,7 +200,7 @@ suite('Editor Model - Model Modes 2', () => { 'Line5'; const LANGUAGE_ID = 'modelModeTest2'; languageRegistration = modes.TokenizationRegistry.register(LANGUAGE_ID, tokenizationSupport); - thisModel = TextModel.createFromString(TEXT, undefined, new modes.LanguageIdentifier(LANGUAGE_ID, 0)); + thisModel = createTextModel(TEXT, undefined, new modes.LanguageIdentifier(LANGUAGE_ID, 0)); }); teardown(() => { diff --git a/src/vs/editor/test/common/model/model.test.ts b/src/vs/editor/test/common/model/model.test.ts index 7fb877d7fd8..77ce4133e46 100644 --- a/src/vs/editor/test/common/model/model.test.ts +++ b/src/vs/editor/test/common/model/model.test.ts @@ -15,6 +15,7 @@ import { IState, LanguageIdentifier, MetadataConsts, TokenizationRegistry } from import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { NULL_STATE } from 'vs/editor/common/modes/nullMode'; import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; // --------- utils @@ -35,7 +36,7 @@ suite('Editor Model - Model', () => { LINE3 + '\n' + LINE4 + '\r\n' + LINE5; - thisModel = TextModel.createFromString(text); + thisModel = createTextModel(text); }); teardown(() => { @@ -349,7 +350,7 @@ suite('Editor Model - Model Line Separators', () => { LINE3 + '\u2028' + LINE4 + '\r\n' + LINE5; - thisModel = TextModel.createFromString(text); + thisModel = createTextModel(text); }); teardown(() => { @@ -365,7 +366,7 @@ suite('Editor Model - Model Line Separators', () => { }); test('Bug 13333:Model should line break on lonely CR too', () => { - let model = TextModel.createFromString('Hello\rWorld!\r\nAnother line'); + let model = createTextModel('Hello\rWorld!\r\nAnother line'); assert.equal(model.getLineCount(), 3); assert.equal(model.getValue(), 'Hello\r\nWorld!\r\nAnother line'); model.dispose(); @@ -430,7 +431,7 @@ suite('Editor Model - Words', () => { test('Get word at position', () => { const text = ['This text has some words. ']; - const thisModel = TextModel.createFromString(text.join('\n')); + const thisModel = createTextModel(text.join('\n')); disposables.push(thisModel); assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 1)), { word: 'This', startColumn: 1, endColumn: 5 }); @@ -451,7 +452,7 @@ suite('Editor Model - Words', () => { const innerMode = new InnerMode(); disposables.push(outerMode, innerMode); - const model = TextModel.createFromString('abab', undefined, outerMode.getLanguageIdentifier()); + const model = createTextModel('abab', undefined, outerMode.getLanguageIdentifier()); disposables.push(model); assert.deepEqual(model.getWordAtPosition(new Position(1, 1)), { word: 'ab', startColumn: 1, endColumn: 3 }); @@ -476,7 +477,7 @@ suite('Editor Model - Words', () => { }; disposables.push(mode); - const thisModel = TextModel.createFromString('.🐷-a-b', undefined, MODE_ID); + const thisModel = createTextModel('.🐷-a-b', undefined, MODE_ID); disposables.push(thisModel); assert.deepEqual(thisModel.getWordAtPosition(new Position(1, 1)), { word: '.', startColumn: 1, endColumn: 2 }); diff --git a/src/vs/editor/test/common/model/modelDecorations.test.ts b/src/vs/editor/test/common/model/modelDecorations.test.ts index afe58fd37d7..4f7690f1111 100644 --- a/src/vs/editor/test/common/model/modelDecorations.test.ts +++ b/src/vs/editor/test/common/model/modelDecorations.test.ts @@ -9,6 +9,7 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { EndOfLineSequence, IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/model'; import { TextModel } from 'vs/editor/common/model/textModel'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; // --------- utils @@ -92,7 +93,7 @@ suite('Editor Model - Model Decorations', () => { LINE3 + '\n' + LINE4 + '\r\n' + LINE5; - thisModel = TextModel.createFromString(text); + thisModel = createTextModel(text); }); teardown(() => { @@ -400,7 +401,7 @@ suite('Editor Model - Model Decorations', () => { }); test('removeAllDecorationsWithOwnerId can be called after model dispose', () => { - let model = TextModel.createFromString('asd'); + let model = createTextModel('asd'); model.dispose(); model.removeAllDecorationsWithOwnerId(1); }); @@ -415,7 +416,7 @@ suite('Editor Model - Model Decorations', () => { suite('Decorations and editing', () => { function _runTest(decRange: Range, stickiness: TrackedRangeStickiness, editRange: Range, editText: string, editForceMoveMarkers: boolean, expectedDecRange: Range, msg: string): void { - let model = TextModel.createFromString([ + let model = createTextModel([ 'My First Line', 'My Second Line', 'Third Line' @@ -1148,7 +1149,7 @@ suite('deltaDecorations', () => { function testDeltaDecorations(text: string[], decorations: ILightWeightDecoration[], newDecorations: ILightWeightDecoration[]): void { - let model = TextModel.createFromString(text.join('\n')); + let model = createTextModel(text.join('\n')); // Add initial decorations & assert they are added let initialIds = model.deltaDecorations([], decorations.map(toModelDeltaDecoration)); @@ -1177,7 +1178,7 @@ suite('deltaDecorations', () => { } test('result respects input', () => { - let model = TextModel.createFromString([ + let model = createTextModel([ 'Hello world,', 'How are you?' ].join('\n')); @@ -1265,7 +1266,7 @@ suite('deltaDecorations', () => { test('issue #4317: editor.setDecorations doesn\'t update the hover message', () => { - let model = TextModel.createFromString('Hello world!'); + let model = createTextModel('Hello world!'); let ids = model.deltaDecorations([], [{ range: { @@ -1299,7 +1300,7 @@ suite('deltaDecorations', () => { }); test('model doesn\'t get confused with individual tracked ranges', () => { - let model = TextModel.createFromString([ + let model = createTextModel([ 'Hello world,', 'How are you?' ].join('\n')); @@ -1340,7 +1341,7 @@ suite('deltaDecorations', () => { }); test('issue #16922: Clicking on link doesn\'t seem to do anything', () => { - let model = TextModel.createFromString([ + let model = createTextModel([ 'Hello world,', 'How are you?', 'Fine.', @@ -1371,7 +1372,7 @@ suite('deltaDecorations', () => { test('issue #41492: URL highlighting persists after pasting over url', () => { - let model = TextModel.createFromString([ + let model = createTextModel([ 'My First Line' ].join('\n')); diff --git a/src/vs/editor/test/common/model/modelEditOperation.test.ts b/src/vs/editor/test/common/model/modelEditOperation.test.ts index 4340d6bbbeb..2fc1337cbe2 100644 --- a/src/vs/editor/test/common/model/modelEditOperation.test.ts +++ b/src/vs/editor/test/common/model/modelEditOperation.test.ts @@ -6,6 +6,7 @@ import * as assert from 'assert'; import { Range } from 'vs/editor/common/core/range'; import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; import { TextModel } from 'vs/editor/common/model/textModel'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; suite('Editor Model - Model Edit Operation', () => { const LINE1 = 'My First Line'; @@ -23,7 +24,7 @@ suite('Editor Model - Model Edit Operation', () => { LINE3 + '\n' + LINE4 + '\r\n' + LINE5; - model = TextModel.createFromString(text); + model = createTextModel(text); }); teardown(() => { diff --git a/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts index e24401abda1..ddec1faccd1 100644 --- a/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts +++ b/src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts @@ -12,7 +12,7 @@ import { PieceTreeBase } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceT import { PieceTreeTextBuffer } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer'; import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; import { NodeColor, SENTINEL, TreeNode } from 'vs/editor/common/model/pieceTreeTextBuffer/rbTreeBase'; -import { TextModel } from 'vs/editor/common/model/textModel'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { SearchData } from 'vs/editor/common/model/textModelSearch'; const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\r\n'; @@ -1761,7 +1761,7 @@ function getValueInSnapshot(snapshot: ITextSnapshot) { } suite('snapshot', () => { test('bug #45564, piece tree pieces should be immutable', () => { - const model = TextModel.createFromString('\n'); + const model = createTextModel('\n'); model.applyEdits([ { range: new Range(2, 1, 2, 1), @@ -1789,7 +1789,7 @@ suite('snapshot', () => { }); test('immutable snapshot 1', () => { - const model = TextModel.createFromString('abc\ndef'); + const model = createTextModel('abc\ndef'); const snapshot = model.createSnapshot(); model.applyEdits([ { @@ -1809,7 +1809,7 @@ suite('snapshot', () => { }); test('immutable snapshot 2', () => { - const model = TextModel.createFromString('abc\ndef'); + const model = createTextModel('abc\ndef'); const snapshot = model.createSnapshot(); model.applyEdits([ { @@ -1829,7 +1829,7 @@ suite('snapshot', () => { }); test('immutable snapshot 3', () => { - const model = TextModel.createFromString('abc\ndef'); + const model = createTextModel('abc\ndef'); model.applyEdits([ { range: new Range(2, 4, 2, 4), @@ -1896,4 +1896,4 @@ suite('chunk based search', () => { assert.equal(ret.length, 1); assert.deepEqual(ret[0].range, new Range(2, 2, 2, 3)); }); -}); \ No newline at end of file +}); diff --git a/src/vs/editor/test/common/model/textModel.test.ts b/src/vs/editor/test/common/model/textModel.test.ts index bf4ee50506c..2da334170d2 100644 --- a/src/vs/editor/test/common/model/textModel.test.ts +++ b/src/vs/editor/test/common/model/textModel.test.ts @@ -163,7 +163,7 @@ suite('Editor Model - TextModel', () => { test('getValueLengthInRange', () => { - let m = TextModel.createFromString('My First Line\r\nMy Second Line\r\nMy Third Line'); + let m = createTextModel('My First Line\r\nMy Second Line\r\nMy Third Line'); assert.equal(m.getValueLengthInRange(new Range(1, 1, 1, 1)), ''.length); assert.equal(m.getValueLengthInRange(new Range(1, 1, 1, 2)), 'M'.length); assert.equal(m.getValueLengthInRange(new Range(1, 2, 1, 3)), 'y'.length); @@ -176,7 +176,7 @@ suite('Editor Model - TextModel', () => { assert.equal(m.getValueLengthInRange(new Range(1, 2, 3, 1000)), 'y First Line\r\nMy Second Line\r\nMy Third Line'.length); assert.equal(m.getValueLengthInRange(new Range(1, 1, 1000, 1000)), 'My First Line\r\nMy Second Line\r\nMy Third Line'.length); - m = TextModel.createFromString('My First Line\nMy Second Line\nMy Third Line'); + m = createTextModel('My First Line\nMy Second Line\nMy Third Line'); assert.equal(m.getValueLengthInRange(new Range(1, 1, 1, 1)), ''.length); assert.equal(m.getValueLengthInRange(new Range(1, 1, 1, 2)), 'M'.length); assert.equal(m.getValueLengthInRange(new Range(1, 2, 1, 3)), 'y'.length); @@ -662,7 +662,7 @@ suite('Editor Model - TextModel', () => { test('validatePosition', () => { - let m = TextModel.createFromString('line one\nline two'); + let m = createTextModel('line one\nline two'); assert.deepEqual(m.validatePosition(new Position(0, 0)), new Position(1, 1)); assert.deepEqual(m.validatePosition(new Position(0, 1)), new Position(1, 1)); @@ -691,7 +691,7 @@ suite('Editor Model - TextModel', () => { test('validatePosition around high-low surrogate pairs 1', () => { - let m = TextModel.createFromString('a📚b'); + let m = createTextModel('a📚b'); assert.deepEqual(m.validatePosition(new Position(0, 0)), new Position(1, 1)); assert.deepEqual(m.validatePosition(new Position(0, 1)), new Position(1, 1)); @@ -718,7 +718,7 @@ suite('Editor Model - TextModel', () => { test('validatePosition around high-low surrogate pairs 2', () => { - let m = TextModel.createFromString('a📚📚b'); + let m = createTextModel('a📚📚b'); assert.deepEqual(m.validatePosition(new Position(1, 1)), new Position(1, 1)); assert.deepEqual(m.validatePosition(new Position(1, 2)), new Position(1, 2)); @@ -732,7 +732,7 @@ suite('Editor Model - TextModel', () => { test('validatePosition handle NaN.', () => { - let m = TextModel.createFromString('line one\nline two'); + let m = createTextModel('line one\nline two'); assert.deepEqual(m.validatePosition(new Position(NaN, 1)), new Position(1, 1)); assert.deepEqual(m.validatePosition(new Position(1, NaN)), new Position(1, 1)); @@ -743,7 +743,7 @@ suite('Editor Model - TextModel', () => { }); test('issue #71480: validatePosition handle floats', () => { - let m = TextModel.createFromString('line one\nline two'); + let m = createTextModel('line one\nline two'); assert.deepEqual(m.validatePosition(new Position(0.2, 1)), new Position(1, 1), 'a'); assert.deepEqual(m.validatePosition(new Position(1.2, 1)), new Position(1, 1), 'b'); @@ -756,7 +756,7 @@ suite('Editor Model - TextModel', () => { }); test('issue #71480: validateRange handle floats', () => { - let m = TextModel.createFromString('line one\nline two'); + let m = createTextModel('line one\nline two'); assert.deepEqual(m.validateRange(new Range(0.2, 1.5, 0.8, 2.5)), new Range(1, 1, 1, 1)); assert.deepEqual(m.validateRange(new Range(1.2, 1.7, 1.8, 2.2)), new Range(1, 1, 1, 2)); @@ -764,7 +764,7 @@ suite('Editor Model - TextModel', () => { test('validateRange around high-low surrogate pairs 1', () => { - let m = TextModel.createFromString('a📚b'); + let m = createTextModel('a📚b'); assert.deepEqual(m.validateRange(new Range(0, 0, 0, 1)), new Range(1, 1, 1, 1)); assert.deepEqual(m.validateRange(new Range(0, 0, 0, 7)), new Range(1, 1, 1, 1)); @@ -792,7 +792,7 @@ suite('Editor Model - TextModel', () => { test('validateRange around high-low surrogate pairs 2', () => { - let m = TextModel.createFromString('a📚📚b'); + let m = createTextModel('a📚📚b'); assert.deepEqual(m.validateRange(new Range(0, 0, 0, 1)), new Range(1, 1, 1, 1)); assert.deepEqual(m.validateRange(new Range(0, 0, 0, 7)), new Range(1, 1, 1, 1)); @@ -835,7 +835,7 @@ suite('Editor Model - TextModel', () => { test('modifyPosition', () => { - let m = TextModel.createFromString('line one\nline two'); + let m = createTextModel('line one\nline two'); assert.deepEqual(m.modifyPosition(new Position(1, 1), 0), new Position(1, 1)); assert.deepEqual(m.modifyPosition(new Position(0, 0), 0), new Position(1, 1)); assert.deepEqual(m.modifyPosition(new Position(30, 1), 0), new Position(2, 9)); @@ -913,7 +913,7 @@ suite('Editor Model - TextModel', () => { }); test('getLineFirstNonWhitespaceColumn', () => { - let model = TextModel.createFromString([ + let model = createTextModel([ 'asd', ' asd', '\tasd', @@ -943,7 +943,7 @@ suite('Editor Model - TextModel', () => { }); test('getLineLastNonWhitespaceColumn', () => { - let model = TextModel.createFromString([ + let model = createTextModel([ 'asd', 'asd ', 'asd\t', @@ -973,7 +973,7 @@ suite('Editor Model - TextModel', () => { }); test('#50471. getValueInRange with invalid range', () => { - let m = TextModel.createFromString('My First Line\r\nMy Second Line\r\nMy Third Line'); + let m = createTextModel('My First Line\r\nMy Second Line\r\nMy Third Line'); assert.equal(m.getValueInRange(new Range(1, NaN, 1, 3)), 'My'); assert.equal(m.getValueInRange(new Range(NaN, NaN, NaN, NaN)), ''); }); @@ -982,24 +982,24 @@ suite('Editor Model - TextModel', () => { suite('TextModel.mightContainRTL', () => { test('nope', () => { - let model = TextModel.createFromString('hello world!'); + let model = createTextModel('hello world!'); assert.equal(model.mightContainRTL(), false); }); test('yes', () => { - let model = TextModel.createFromString('Hello,\nזוהי עובדה מבוססת שדעתו'); + let model = createTextModel('Hello,\nזוהי עובדה מבוססת שדעתו'); assert.equal(model.mightContainRTL(), true); }); test('setValue resets 1', () => { - let model = TextModel.createFromString('hello world!'); + let model = createTextModel('hello world!'); assert.equal(model.mightContainRTL(), false); model.setValue('Hello,\nזוהי עובדה מבוססת שדעתו'); assert.equal(model.mightContainRTL(), true); }); test('setValue resets 2', () => { - let model = TextModel.createFromString('Hello,\nهناك حقيقة مثبتة منذ زمن طويل'); + let model = createTextModel('Hello,\nهناك حقيقة مثبتة منذ زمن طويل'); assert.equal(model.mightContainRTL(), true); model.setValue('hello world!'); assert.equal(model.mightContainRTL(), false); @@ -1010,14 +1010,14 @@ suite('TextModel.mightContainRTL', () => { suite('TextModel.createSnapshot', () => { test('empty file', () => { - let model = TextModel.createFromString(''); + let model = createTextModel(''); let snapshot = model.createSnapshot(); assert.equal(snapshot.read(), null); model.dispose(); }); test('file with BOM', () => { - let model = TextModel.createFromString(UTF8_BOM_CHARACTER + 'Hello'); + let model = createTextModel(UTF8_BOM_CHARACTER + 'Hello'); assert.equal(model.getLineContent(1), 'Hello'); let snapshot = model.createSnapshot(true); assert.equal(snapshot.read(), UTF8_BOM_CHARACTER + 'Hello'); @@ -1026,7 +1026,7 @@ suite('TextModel.createSnapshot', () => { }); test('regular file', () => { - let model = TextModel.createFromString('My First Line\n\t\tMy Second Line\n Third Line\n\n1'); + let model = createTextModel('My First Line\n\t\tMy Second Line\n Third Line\n\n1'); let snapshot = model.createSnapshot(); assert.equal(snapshot.read(), 'My First Line\n\t\tMy Second Line\n Third Line\n\n1'); assert.equal(snapshot.read(), null); @@ -1040,7 +1040,7 @@ suite('TextModel.createSnapshot', () => { } const text = lines.join('\n'); - let model = TextModel.createFromString(text); + let model = createTextModel(text); let snapshot = model.createSnapshot(); let actual = ''; diff --git a/src/vs/editor/test/common/model/textModelSearch.test.ts b/src/vs/editor/test/common/model/textModelSearch.test.ts index 2a55d718b50..e5af37e781b 100644 --- a/src/vs/editor/test/common/model/textModelSearch.test.ts +++ b/src/vs/editor/test/common/model/textModelSearch.test.ts @@ -11,6 +11,7 @@ import { EndOfLineSequence, FindMatch } from 'vs/editor/common/model'; import { TextModel } from 'vs/editor/common/model/textModel'; import { SearchData, SearchParams, TextModelSearch, isMultilineRegexSource } from 'vs/editor/common/model/textModelSearch'; import { USUAL_WORD_SEPARATORS } from 'vs/editor/common/model/wordHelper'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; // --------- Find suite('TextModelSearch', () => { @@ -51,12 +52,12 @@ suite('TextModelSearch', () => { let expectedMatches = expectedRanges.map(entry => new FindMatch(entry, null)); let searchParams = new SearchParams(searchString, isRegex, matchCase, wordSeparators); - let model = TextModel.createFromString(text); + let model = createTextModel(text); _assertFindMatches(model, searchParams, expectedMatches); model.dispose(); - let model2 = TextModel.createFromString(text); + let model2 = createTextModel(text); model2.setEOL(EndOfLineSequence.CRLF); _assertFindMatches(model2, searchParams, expectedMatches); model2.dispose(); @@ -380,7 +381,7 @@ suite('TextModelSearch', () => { }); test('findNextMatch without regex', () => { - let model = TextModel.createFromString('line line one\nline two\nthree'); + let model = createTextModel('line line one\nline two\nthree'); let searchParams = new SearchParams('line', false, false, null); @@ -403,7 +404,7 @@ suite('TextModelSearch', () => { }); test('findNextMatch with beginning boundary regex', () => { - let model = TextModel.createFromString('line one\nline two\nthree'); + let model = createTextModel('line one\nline two\nthree'); let searchParams = new SearchParams('^line', true, false, null); @@ -423,7 +424,7 @@ suite('TextModelSearch', () => { }); test('findNextMatch with beginning boundary regex and line has repetitive beginnings', () => { - let model = TextModel.createFromString('line line one\nline two\nthree'); + let model = createTextModel('line line one\nline two\nthree'); let searchParams = new SearchParams('^line', true, false, null); @@ -443,7 +444,7 @@ suite('TextModelSearch', () => { }); test('findNextMatch with beginning boundary multiline regex and line has repetitive beginnings', () => { - let model = TextModel.createFromString('line line one\nline two\nline three\nline four'); + let model = createTextModel('line line one\nline two\nline three\nline four'); let searchParams = new SearchParams('^line.*\\nline', true, false, null); @@ -460,7 +461,7 @@ suite('TextModelSearch', () => { }); test('findNextMatch with ending boundary regex', () => { - let model = TextModel.createFromString('one line line\ntwo line\nthree'); + let model = createTextModel('one line line\ntwo line\nthree'); let searchParams = new SearchParams('line$', true, false, null); @@ -480,7 +481,7 @@ suite('TextModelSearch', () => { }); test('findMatches with capturing matches', () => { - let model = TextModel.createFromString('one line line\ntwo line\nthree'); + let model = createTextModel('one line line\ntwo line\nthree'); let searchParams = new SearchParams('(l(in)e)', true, false, null); @@ -495,7 +496,7 @@ suite('TextModelSearch', () => { }); test('findMatches multiline with capturing matches', () => { - let model = TextModel.createFromString('one line line\ntwo line\nthree'); + let model = createTextModel('one line line\ntwo line\nthree'); let searchParams = new SearchParams('(l(in)e)\\n', true, false, null); @@ -509,7 +510,7 @@ suite('TextModelSearch', () => { }); test('findNextMatch with capturing matches', () => { - let model = TextModel.createFromString('one line line\ntwo line\nthree'); + let model = createTextModel('one line line\ntwo line\nthree'); let searchParams = new SearchParams('(l(in)e)', true, false, null); @@ -520,7 +521,7 @@ suite('TextModelSearch', () => { }); test('findNextMatch multiline with capturing matches', () => { - let model = TextModel.createFromString('one line line\ntwo line\nthree'); + let model = createTextModel('one line line\ntwo line\nthree'); let searchParams = new SearchParams('(l(in)e)\\n', true, false, null); @@ -531,7 +532,7 @@ suite('TextModelSearch', () => { }); test('findPreviousMatch with capturing matches', () => { - let model = TextModel.createFromString('one line line\ntwo line\nthree'); + let model = createTextModel('one line line\ntwo line\nthree'); let searchParams = new SearchParams('(l(in)e)', true, false, null); @@ -542,7 +543,7 @@ suite('TextModelSearch', () => { }); test('findPreviousMatch multiline with capturing matches', () => { - let model = TextModel.createFromString('one line line\ntwo line\nthree'); + let model = createTextModel('one line line\ntwo line\nthree'); let searchParams = new SearchParams('(l(in)e)\\n', true, false, null); @@ -553,7 +554,7 @@ suite('TextModelSearch', () => { }); test('\\n matches \\r\\n', () => { - let model = TextModel.createFromString('a\r\nb\r\nc\r\nd\r\ne\r\nf\r\ng\r\nh\r\ni'); + let model = createTextModel('a\r\nb\r\nc\r\nd\r\ne\r\nf\r\ng\r\nh\r\ni'); assert.equal(model.getEOL(), '\r\n'); @@ -576,7 +577,7 @@ suite('TextModelSearch', () => { }); test('\\r can never be found', () => { - let model = TextModel.createFromString('a\r\nb\r\nc\r\nd\r\ne\r\nf\r\ng\r\nh\r\ni'); + let model = createTextModel('a\r\nb\r\nc\r\nd\r\ne\r\nf\r\ng\r\nh\r\ni'); assert.equal(model.getEOL(), '\r\n'); @@ -763,7 +764,7 @@ suite('TextModelSearch', () => { }); test('issue #74715. \\d* finds empty string and stops searching.', () => { - let model = TextModel.createFromString('10.243.30.10'); + let model = createTextModel('10.243.30.10'); let searchParams = new SearchParams('\\d*', true, false, null); diff --git a/src/vs/editor/test/common/model/textModelWithTokens.test.ts b/src/vs/editor/test/common/model/textModelWithTokens.test.ts index 09353818317..815c8a073e4 100644 --- a/src/vs/editor/test/common/model/textModelWithTokens.test.ts +++ b/src/vs/editor/test/common/model/textModelWithTokens.test.ts @@ -15,6 +15,7 @@ import { CharacterPair } from 'vs/editor/common/modes/languageConfiguration'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { NULL_STATE } from 'vs/editor/common/modes/nullMode'; import { ViewLineToken } from 'vs/editor/test/common/core/viewLineToken'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; suite('TextModelWithTokens', () => { @@ -72,7 +73,7 @@ suite('TextModelWithTokens', () => { brackets: brackets }); - let model = TextModel.createFromString( + let model = createTextModel( contents.join('\n'), TextModel.DEFAULT_CREATION_OPTIONS, languageIdentifier @@ -178,7 +179,7 @@ suite('TextModelWithTokens - bracket matching', () => { let text = ')]}{[(' + '\n' + ')]}{[('; - let model = TextModel.createFromString(text, undefined, languageIdentifier); + let model = createTextModel(text, undefined, languageIdentifier); assertIsNotBracket(model, 1, 1); assertIsNotBracket(model, 1, 2); @@ -206,7 +207,7 @@ suite('TextModelWithTokens - bracket matching', () => { '}, bar: {hallo: [{' + '\n' + '}, {' + '\n' + '}]}}'; - let model = TextModel.createFromString(text, undefined, languageIdentifier); + let model = createTextModel(text, undefined, languageIdentifier); let brackets: [Position, Range, Range][] = [ [new Position(1, 11), new Range(1, 11, 1, 12), new Range(5, 4, 5, 5)], @@ -284,7 +285,7 @@ suite('TextModelWithTokens', () => { 'end;', ].join('\n'); - const model = TextModel.createFromString(text, undefined, languageIdentifier); + const model = createTextModel(text, undefined, languageIdentifier); // ... is not matched assertIsNotBracket(model, 10, 9); @@ -322,7 +323,7 @@ suite('TextModelWithTokens', () => { 'endrecord', ].join('\n'); - const model = TextModel.createFromString(text, undefined, languageIdentifier); + const model = createTextModel(text, undefined, languageIdentifier); // ... is matched assertIsBracket(model, new Position(1, 1), [new Range(1, 1, 1, 12), new Range(4, 1, 4, 10)]); @@ -388,7 +389,7 @@ suite('TextModelWithTokens', () => { ], }); - const model = TextModel.createFromString([ + const model = createTextModel([ 'function hello() {', ' console.log(`${100}`);', '}' @@ -459,7 +460,7 @@ suite('TextModelWithTokens regression tests', () => { let registration1 = TokenizationRegistry.register(LANG_ID1, tokenizationSupport); let registration2 = TokenizationRegistry.register(LANG_ID2, tokenizationSupport); - let model = TextModel.createFromString('A model with\ntwo lines'); + let model = createTextModel('A model with\ntwo lines'); assertViewLineTokens(model, 1, true, [createViewLineToken(12, 1)]); assertViewLineTokens(model, 2, true, [createViewLineToken(9, 1)]); @@ -498,7 +499,7 @@ suite('TextModelWithTokens regression tests', () => { ] }); - let model = TextModel.createFromString([ + let model = createTextModel([ 'Imports System', 'Imports System.Collections.Generic', '', @@ -528,7 +529,7 @@ suite('TextModelWithTokens regression tests', () => { ] }); - let model = TextModel.createFromString([ + let model = createTextModel([ 'sequence "outer"', ' sequence "inner"', ' endsequence', @@ -561,7 +562,7 @@ suite('TextModelWithTokens regression tests', () => { let registration = TokenizationRegistry.register(outerMode.language, tokenizationSupport); - let model = TextModel.createFromString('A model with one line', undefined, outerMode); + let model = createTextModel('A model with one line', undefined, outerMode); model.forceTokenization(1); assert.equal(model.getLanguageIdAtPosition(1, 1), innerMode.id); @@ -574,7 +575,7 @@ suite('TextModelWithTokens regression tests', () => { suite('TextModel.getLineIndentGuide', () => { function assertIndentGuides(lines: [number, number, number, number, string][], tabSize: number): void { let text = lines.map(l => l[4]).join('\n'); - let model = TextModel.createFromString(text); + let model = createTextModel(text); model.updateOptions({ tabSize: tabSize }); let actualIndents = model.getLinesIndentGuides(1, model.getLineCount()); @@ -747,7 +748,7 @@ suite('TextModel.getLineIndentGuide', () => { }); test('issue #49173', () => { - let model = TextModel.createFromString([ + let model = createTextModel([ 'class A {', ' public m1(): void {', ' }', diff --git a/src/vs/editor/test/common/model/tokensStore.test.ts b/src/vs/editor/test/common/model/tokensStore.test.ts index eb81aa1570b..51d85e382f8 100644 --- a/src/vs/editor/test/common/model/tokensStore.test.ts +++ b/src/vs/editor/test/common/model/tokensStore.test.ts @@ -9,6 +9,7 @@ import { Range } from 'vs/editor/common/core/range'; import { TextModel } from 'vs/editor/common/model/textModel'; import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; import { MetadataConsts, TokenMetadata } from 'vs/editor/common/modes'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; suite('TokensStore', () => { @@ -96,7 +97,7 @@ suite('TokensStore', () => { function testTokensAdjustment(rawInitialState: string[], edits: IIdentifiedSingleEditOperation[], rawFinalState: string[]) { const initialState = parseTokensState(rawInitialState); - const model = TextModel.createFromString(initialState.text); + const model = createTextModel(initialState.text); model.setSemanticTokens([initialState.tokens]); model.applyEdits(edits); diff --git a/src/vs/editor/test/common/services/modelService.test.ts b/src/vs/editor/test/common/services/modelService.test.ts index cd2df2cb74e..84ebc8f2e0e 100644 --- a/src/vs/editor/test/common/services/modelService.test.ts +++ b/src/vs/editor/test/common/services/modelService.test.ts @@ -11,7 +11,7 @@ import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; import { createStringBuilder } from 'vs/editor/common/core/stringBuilder'; import { DefaultEndOfLine } from 'vs/editor/common/model'; -import { TextModel, createTextBuffer } from 'vs/editor/common/model/textModel'; +import { createTextBuffer } from 'vs/editor/common/model/textModel'; import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -19,6 +19,9 @@ import { TestConfigurationService } from 'vs/platform/configuration/test/common/ import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { NullLogService } from 'vs/platform/log/common/log'; import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; +import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; const GENERATE_TESTS = false; @@ -30,7 +33,7 @@ suite('ModelService', () => { configService.setUserConfiguration('files', { 'eol': '\n' }); configService.setUserConfiguration('files', { 'eol': '\r\n' }, URI.file(platform.isWindows ? 'c:\\myroot' : '/myroot')); - modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), new UndoRedoService()); + modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), new UndoRedoService(new TestDialogService(), new TestNotificationService())); }); teardown(() => { @@ -49,7 +52,7 @@ suite('ModelService', () => { test('_computeEdits no change', function () { - const model = TextModel.createFromString( + const model = createTextModel( [ 'This is line one', //16 'and this is line number two', //27 @@ -75,7 +78,7 @@ suite('ModelService', () => { test('_computeEdits first line changed', function () { - const model = TextModel.createFromString( + const model = createTextModel( [ 'This is line one', //16 'and this is line number two', //27 @@ -103,7 +106,7 @@ suite('ModelService', () => { test('_computeEdits EOL changed', function () { - const model = TextModel.createFromString( + const model = createTextModel( [ 'This is line one', //16 'and this is line number two', //27 @@ -129,7 +132,7 @@ suite('ModelService', () => { test('_computeEdits EOL and other change 1', function () { - const model = TextModel.createFromString( + const model = createTextModel( [ 'This is line one', //16 'and this is line number two', //27 @@ -165,7 +168,7 @@ suite('ModelService', () => { test('_computeEdits EOL and other change 2', function () { - const model = TextModel.createFromString( + const model = createTextModel( [ 'package main', // 1 'func foo() {', // 2 @@ -307,7 +310,7 @@ suite('ModelService', () => { }); function assertComputeEdits(lines1: string[], lines2: string[]): void { - const model = TextModel.createFromString(lines1.join('\n')); + const model = createTextModel(lines1.join('\n')); const textBuffer = createTextBuffer(lines2.join('\n'), DefaultEndOfLine.LF); // compute required edits diff --git a/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts b/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts index 053a3ef301a..8be7627bc72 100644 --- a/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts +++ b/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts @@ -18,9 +18,9 @@ suite('Editor ViewLayout - ViewLineParts', () => { ]); assert.deepEqual(result, [ - new DecorationSegment(0, 1, 'c1'), - new DecorationSegment(2, 2, 'c2 c1'), - new DecorationSegment(3, 9, 'c1'), + new DecorationSegment(0, 1, 'c1', 0), + new DecorationSegment(2, 2, 'c2 c1', 0), + new DecorationSegment(3, 9, 'c1', 0), ]); }); @@ -32,8 +32,8 @@ suite('Editor ViewLayout - ViewLineParts', () => { ]); assert.deepEqual(result, [ - new DecorationSegment(14, 18, 'mtkw'), - new DecorationSegment(19, 19, 'mtkw inline-folded') + new DecorationSegment(14, 18, 'mtkw', 0), + new DecorationSegment(19, 19, 'mtkw inline-folded', 0) ]); }); @@ -66,24 +66,24 @@ suite('Editor ViewLayout - ViewLineParts', () => { new LineDecoration(1, 2, 'c1', InlineDecorationType.Regular), new LineDecoration(3, 4, 'c2', InlineDecorationType.Regular) ]), [ - new DecorationSegment(0, 0, 'c1'), - new DecorationSegment(2, 2, 'c2') + new DecorationSegment(0, 0, 'c1', 0), + new DecorationSegment(2, 2, 'c2', 0) ]); assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ new LineDecoration(1, 3, 'c1', InlineDecorationType.Regular), new LineDecoration(3, 4, 'c2', InlineDecorationType.Regular) ]), [ - new DecorationSegment(0, 1, 'c1'), - new DecorationSegment(2, 2, 'c2') + new DecorationSegment(0, 1, 'c1', 0), + new DecorationSegment(2, 2, 'c2', 0) ]); assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ new LineDecoration(1, 4, 'c1', InlineDecorationType.Regular), new LineDecoration(3, 4, 'c2', InlineDecorationType.Regular) ]), [ - new DecorationSegment(0, 1, 'c1'), - new DecorationSegment(2, 2, 'c1 c2') + new DecorationSegment(0, 1, 'c1', 0), + new DecorationSegment(2, 2, 'c1 c2', 0) ]); assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ @@ -91,8 +91,8 @@ suite('Editor ViewLayout - ViewLineParts', () => { new LineDecoration(1, 4, 'c1*', InlineDecorationType.Regular), new LineDecoration(3, 4, 'c2', InlineDecorationType.Regular) ]), [ - new DecorationSegment(0, 1, 'c1 c1*'), - new DecorationSegment(2, 2, 'c1 c1* c2') + new DecorationSegment(0, 1, 'c1 c1*', 0), + new DecorationSegment(2, 2, 'c1 c1* c2', 0) ]); assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ @@ -101,8 +101,8 @@ suite('Editor ViewLayout - ViewLineParts', () => { new LineDecoration(1, 4, 'c1**', InlineDecorationType.Regular), new LineDecoration(3, 4, 'c2', InlineDecorationType.Regular) ]), [ - new DecorationSegment(0, 1, 'c1 c1* c1**'), - new DecorationSegment(2, 2, 'c1 c1* c1** c2') + new DecorationSegment(0, 1, 'c1 c1* c1**', 0), + new DecorationSegment(2, 2, 'c1 c1* c1** c2', 0) ]); assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ @@ -112,8 +112,8 @@ suite('Editor ViewLayout - ViewLineParts', () => { new LineDecoration(3, 4, 'c2', InlineDecorationType.Regular), new LineDecoration(3, 4, 'c2*', InlineDecorationType.Regular) ]), [ - new DecorationSegment(0, 1, 'c1 c1* c1**'), - new DecorationSegment(2, 2, 'c1 c1* c1** c2 c2*') + new DecorationSegment(0, 1, 'c1 c1* c1**', 0), + new DecorationSegment(2, 2, 'c1 c1* c1** c2 c2*', 0) ]); assert.deepEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ @@ -123,9 +123,9 @@ suite('Editor ViewLayout - ViewLineParts', () => { new LineDecoration(3, 4, 'c2', InlineDecorationType.Regular), new LineDecoration(3, 5, 'c2*', InlineDecorationType.Regular) ]), [ - new DecorationSegment(0, 1, 'c1 c1* c1**'), - new DecorationSegment(2, 2, 'c1 c1* c1** c2 c2*'), - new DecorationSegment(3, 3, 'c2*') + new DecorationSegment(0, 1, 'c1 c1* c1**', 0), + new DecorationSegment(2, 2, 'c1 c1* c1** c2 c2*', 0), + new DecorationSegment(3, 3, 'c2*', 0) ]); }); }); diff --git a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts index f52524badd0..f5e4acbc157 100644 --- a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts +++ b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts @@ -388,6 +388,66 @@ suite('viewLineRenderer.renderLine', () => { assertCharacterMapping(_actual.characterMapping, expectedOffsetsArr, [12, 12, 24, 1, 21, 2, 1, 20, 1, 1]); }); + test('issue #91178: after decoration type shown before cursor', () => { + const lineText = '//just a comment'; + const lineParts = createViewLineTokens([ + createPart(16, 1) + ]); + const expectedOutput = [ + '//just\u00a0a\u00a0com', + '', + '', + 'ment', + ].join(''); + + const expectedCharacterMapping = new CharacterMapping(17, 4); + expectedCharacterMapping.setPartData(0, 0, 0, 0); + expectedCharacterMapping.setPartData(1, 0, 1, 0); + expectedCharacterMapping.setPartData(2, 0, 2, 0); + expectedCharacterMapping.setPartData(3, 0, 3, 0); + expectedCharacterMapping.setPartData(4, 0, 4, 0); + expectedCharacterMapping.setPartData(5, 0, 5, 0); + expectedCharacterMapping.setPartData(6, 0, 6, 0); + expectedCharacterMapping.setPartData(7, 0, 7, 0); + expectedCharacterMapping.setPartData(8, 0, 8, 0); + expectedCharacterMapping.setPartData(9, 0, 9, 0); + expectedCharacterMapping.setPartData(10, 0, 10, 0); + expectedCharacterMapping.setPartData(11, 0, 11, 0); + expectedCharacterMapping.setPartData(12, 2, 0, 12); + expectedCharacterMapping.setPartData(13, 3, 1, 12); + expectedCharacterMapping.setPartData(14, 3, 2, 12); + expectedCharacterMapping.setPartData(15, 3, 3, 12); + expectedCharacterMapping.setPartData(16, 3, 4, 12); + + const actual = renderViewLine(new RenderLineInput( + true, + false, + lineText, + false, + true, + false, + 0, + lineParts, + [ + new LineDecoration(13, 13, 'dec1', InlineDecorationType.After), + new LineDecoration(13, 13, 'dec2', InlineDecorationType.Before), + ], + 4, + 0, + 10, + 10, + 10, + -1, + 'none', + false, + false, + null + )); + + assert.equal(actual.html, '' + expectedOutput + ''); + assertCharacterMapping2(actual.characterMapping, expectedCharacterMapping); + }); + test('issue Microsoft/monaco-editor#280: Improved source code rendering for RTL languages', () => { let lineText = 'var קודמות = \"מיותר קודמות צ\'ט של, אם לשון העברית שינויים ויש, אם\";'; @@ -693,6 +753,33 @@ suite('viewLineRenderer.renderLine', () => { assert.equal(_actual.html, '' + expectedOutput + ''); }); + interface ICharMappingData { + charOffset: number; + partIndex: number; + charIndex: number; + } + + function decodeCharacterMapping(source: CharacterMapping) { + const mapping: ICharMappingData[] = []; + for (let charOffset = 0; charOffset < source.length; charOffset++) { + const partData = source.charOffsetToPartData(charOffset); + const partIndex = CharacterMapping.getPartIndex(partData); + const charIndex = CharacterMapping.getCharIndex(partData); + mapping.push({ charOffset, partIndex, charIndex }); + } + const absoluteOffsets: number[] = []; + for (const absoluteOffset of source.getAbsoluteOffsets()) { + absoluteOffsets.push(absoluteOffset); + } + return { mapping, absoluteOffsets }; + } + + function assertCharacterMapping2(actual: CharacterMapping, expected: CharacterMapping): void { + const _actual = decodeCharacterMapping(actual); + const _expected = decodeCharacterMapping(expected); + assert.deepEqual(_actual, _expected); + } + function assertCharacterMapping(actual: CharacterMapping, expectedCharPartOffsets: number[][], expectedPartLengths: number[]): void { assertCharPartOffsets(actual, expectedCharPartOffsets); diff --git a/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts b/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts index 393d6997976..2c5cca1cb71 100644 --- a/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts +++ b/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts @@ -18,6 +18,7 @@ import { LineBreakData, ISimpleModel, SplitLine, SplitLinesCollection } from 'vs import { ViewLineData } from 'vs/editor/common/viewModel/viewModel'; import { TestConfiguration } from 'vs/editor/test/common/mocks/testConfiguration'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; suite('Editor ViewModel - SplitLinesCollection', () => { test('SplitLine', () => { @@ -96,7 +97,7 @@ suite('Editor ViewModel - SplitLinesCollection', () => { const lineBreaksComputerFactory = new MonospaceLineBreaksComputerFactory(wordWrapBreakBeforeCharacters, wordWrapBreakAfterCharacters); - const model = TextModel.createFromString([ + const model = createTextModel([ 'int main() {', '\tprintf("Hello world!");', '}', @@ -347,7 +348,7 @@ suite('SplitLinesCollection', () => { }; const LANGUAGE_ID = 'modelModeTest1'; languageRegistration = modes.TokenizationRegistry.register(LANGUAGE_ID, tokenizationSupport); - model = TextModel.createFromString(_text.join('\n'), undefined, new modes.LanguageIdentifier(LANGUAGE_ID, 0)); + model = createTextModel(_text.join('\n'), undefined, new modes.LanguageIdentifier(LANGUAGE_ID, 0)); // force tokenization model.forceTokenization(model.getLineCount()); }); diff --git a/src/vs/editor/test/common/viewModel/testViewModel.ts b/src/vs/editor/test/common/viewModel/testViewModel.ts index b316d88b004..38f1fc586f1 100644 --- a/src/vs/editor/test/common/viewModel/testViewModel.ts +++ b/src/vs/editor/test/common/viewModel/testViewModel.ts @@ -8,12 +8,13 @@ import { TextModel } from 'vs/editor/common/model/textModel'; import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; import { TestConfiguration } from 'vs/editor/test/common/mocks/testConfiguration'; import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/monospaceLineBreaksComputer'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; export function testViewModel(text: string[], options: IEditorOptions, callback: (viewModel: ViewModel, model: TextModel) => void): void { const EDITOR_ID = 1; const configuration = new TestConfiguration(options); - const model = TextModel.createFromString(text.join('\n')); + const model = createTextModel(text.join('\n')); const monospaceLineBreaksComputerFactory = MonospaceLineBreaksComputerFactory.create(configuration.options); const viewModel = new ViewModel(EDITOR_ID, configuration, model, monospaceLineBreaksComputerFactory, monospaceLineBreaksComputerFactory, null!); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 628fea221a7..fd704b03bd3 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -1884,7 +1884,7 @@ declare namespace monaco.editor { * @param cursorStateComputer A callback that can compute the resulting cursors state after the edit operations have been executed. * @return The cursor state returned by the `cursorStateComputer`. */ - pushEditOperations(beforeCursorState: Selection[], editOperations: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer): Selection[] | null; + pushEditOperations(beforeCursorState: Selection[] | null, editOperations: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer): Selection[] | null; /** * Change the end of line sequence. This is the preferred way of * changing the eol sequence. This will land on the undo stack. @@ -2849,6 +2849,11 @@ declare namespace monaco.editor { * Defaults to true. */ scrollPredominantAxis?: boolean; + /** + * Enable that the selection with the mouse and keys is doing column selection. + * Defaults to false. + */ + columnSelection?: boolean; /** * The modifier to be used to add multiple cursors with the mouse. * Defaults to 'alt' @@ -3026,6 +3031,11 @@ declare namespace monaco.editor { * Defaults to 'mouseover'. */ showFoldingControls?: 'always' | 'mouseover'; + /** + * Controls whether clicking on the empty content after a folded line will unfold the line. + * Defaults to false. + */ + unfoldOnClickAfterEndOfLine?: boolean; /** * Enable highlighting of matching brackets. * Defaults to 'always'. @@ -3794,105 +3804,107 @@ declare namespace monaco.editor { autoSurround = 10, codeLens = 11, colorDecorators = 12, - comments = 13, - contextmenu = 14, - copyWithSyntaxHighlighting = 15, - cursorBlinking = 16, - cursorSmoothCaretAnimation = 17, - cursorStyle = 18, - cursorSurroundingLines = 19, - cursorSurroundingLinesStyle = 20, - cursorWidth = 21, - disableLayerHinting = 22, - disableMonospaceOptimizations = 23, - dragAndDrop = 24, - emptySelectionClipboard = 25, - extraEditorClassName = 26, - fastScrollSensitivity = 27, - find = 28, - fixedOverflowWidgets = 29, - folding = 30, - foldingStrategy = 31, - foldingHighlight = 32, - fontFamily = 33, - fontInfo = 34, - fontLigatures = 35, - fontSize = 36, - fontWeight = 37, - formatOnPaste = 38, - formatOnType = 39, - glyphMargin = 40, - gotoLocation = 41, - hideCursorInOverviewRuler = 42, - highlightActiveIndentGuide = 43, - hover = 44, - inDiffEditor = 45, - letterSpacing = 46, - lightbulb = 47, - lineDecorationsWidth = 48, - lineHeight = 49, - lineNumbers = 50, - lineNumbersMinChars = 51, - links = 52, - matchBrackets = 53, - minimap = 54, - mouseStyle = 55, - mouseWheelScrollSensitivity = 56, - mouseWheelZoom = 57, - multiCursorMergeOverlapping = 58, - multiCursorModifier = 59, - multiCursorPaste = 60, - occurrencesHighlight = 61, - overviewRulerBorder = 62, - overviewRulerLanes = 63, - padding = 64, - parameterHints = 65, - peekWidgetDefaultFocus = 66, - definitionLinkOpensInPeek = 67, - quickSuggestions = 68, - quickSuggestionsDelay = 69, - readOnly = 70, - renderControlCharacters = 71, - renderIndentGuides = 72, - renderFinalNewline = 73, - renderLineHighlight = 74, - renderValidationDecorations = 75, - renderWhitespace = 76, - revealHorizontalRightPadding = 77, - roundedSelection = 78, - rulers = 79, - scrollbar = 80, - scrollBeyondLastColumn = 81, - scrollBeyondLastLine = 82, - scrollPredominantAxis = 83, - selectionClipboard = 84, - selectionHighlight = 85, - selectOnLineNumbers = 86, - showFoldingControls = 87, - showUnused = 88, - snippetSuggestions = 89, - smoothScrolling = 90, - stopRenderingLineAfter = 91, - suggest = 92, - suggestFontSize = 93, - suggestLineHeight = 94, - suggestOnTriggerCharacters = 95, - suggestSelection = 96, - tabCompletion = 97, - useTabStops = 98, - wordSeparators = 99, - wordWrap = 100, - wordWrapBreakAfterCharacters = 101, - wordWrapBreakBeforeCharacters = 102, - wordWrapColumn = 103, - wordWrapMinified = 104, - wrappingIndent = 105, - wrappingStrategy = 106, - editorClassName = 107, - pixelRatio = 108, - tabFocusMode = 109, - layoutInfo = 110, - wrappingInfo = 111 + columnSelection = 13, + comments = 14, + contextmenu = 15, + copyWithSyntaxHighlighting = 16, + cursorBlinking = 17, + cursorSmoothCaretAnimation = 18, + cursorStyle = 19, + cursorSurroundingLines = 20, + cursorSurroundingLinesStyle = 21, + cursorWidth = 22, + disableLayerHinting = 23, + disableMonospaceOptimizations = 24, + dragAndDrop = 25, + emptySelectionClipboard = 26, + extraEditorClassName = 27, + fastScrollSensitivity = 28, + find = 29, + fixedOverflowWidgets = 30, + folding = 31, + foldingStrategy = 32, + foldingHighlight = 33, + unfoldOnClickAfterEndOfLine = 34, + fontFamily = 35, + fontInfo = 36, + fontLigatures = 37, + fontSize = 38, + fontWeight = 39, + formatOnPaste = 40, + formatOnType = 41, + glyphMargin = 42, + gotoLocation = 43, + hideCursorInOverviewRuler = 44, + highlightActiveIndentGuide = 45, + hover = 46, + inDiffEditor = 47, + letterSpacing = 48, + lightbulb = 49, + lineDecorationsWidth = 50, + lineHeight = 51, + lineNumbers = 52, + lineNumbersMinChars = 53, + links = 54, + matchBrackets = 55, + minimap = 56, + mouseStyle = 57, + mouseWheelScrollSensitivity = 58, + mouseWheelZoom = 59, + multiCursorMergeOverlapping = 60, + multiCursorModifier = 61, + multiCursorPaste = 62, + occurrencesHighlight = 63, + overviewRulerBorder = 64, + overviewRulerLanes = 65, + padding = 66, + parameterHints = 67, + peekWidgetDefaultFocus = 68, + definitionLinkOpensInPeek = 69, + quickSuggestions = 70, + quickSuggestionsDelay = 71, + readOnly = 72, + renderControlCharacters = 73, + renderIndentGuides = 74, + renderFinalNewline = 75, + renderLineHighlight = 76, + renderValidationDecorations = 77, + renderWhitespace = 78, + revealHorizontalRightPadding = 79, + roundedSelection = 80, + rulers = 81, + scrollbar = 82, + scrollBeyondLastColumn = 83, + scrollBeyondLastLine = 84, + scrollPredominantAxis = 85, + selectionClipboard = 86, + selectionHighlight = 87, + selectOnLineNumbers = 88, + showFoldingControls = 89, + showUnused = 90, + snippetSuggestions = 91, + smoothScrolling = 92, + stopRenderingLineAfter = 93, + suggest = 94, + suggestFontSize = 95, + suggestLineHeight = 96, + suggestOnTriggerCharacters = 97, + suggestSelection = 98, + tabCompletion = 99, + useTabStops = 100, + wordSeparators = 101, + wordWrap = 102, + wordWrapBreakAfterCharacters = 103, + wordWrapBreakBeforeCharacters = 104, + wordWrapColumn = 105, + wordWrapMinified = 106, + wrappingIndent = 107, + wrappingStrategy = 108, + editorClassName = 109, + pixelRatio = 110, + tabFocusMode = 111, + layoutInfo = 112, + wrappingInfo = 113 } export const EditorOptions: { acceptSuggestionOnCommitCharacter: IEditorOption; @@ -3908,6 +3920,7 @@ declare namespace monaco.editor { autoSurround: IEditorOption; codeLens: IEditorOption; colorDecorators: IEditorOption; + columnSelection: IEditorOption; comments: IEditorOption; contextmenu: IEditorOption; copyWithSyntaxHighlighting: IEditorOption; @@ -3928,6 +3941,7 @@ declare namespace monaco.editor { folding: IEditorOption; foldingStrategy: IEditorOption; foldingHighlight: IEditorOption; + unfoldOnClickAfterEndOfLine: IEditorOption; fontFamily: IEditorOption; fontInfo: IEditorOption; fontLigatures2: IEditorOption; @@ -5433,7 +5447,7 @@ declare namespace monaco.languages { preselect?: boolean; /** * A string or snippet that should be inserted in a document when selecting - * this completion. When `falsy` the [label](#CompletionItem.label) + * this completion. * is used. */ insertText: string; diff --git a/src/vs/platform/dialogs/test/common/testDialogService.ts b/src/vs/platform/dialogs/test/common/testDialogService.ts new file mode 100644 index 00000000000..0f1cbc0f992 --- /dev/null +++ b/src/vs/platform/dialogs/test/common/testDialogService.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import Severity from 'vs/base/common/severity'; +import { IConfirmation, IConfirmationResult, IDialogService, IDialogOptions, IShowResult } from 'vs/platform/dialogs/common/dialogs'; + +export class TestDialogService implements IDialogService { + + _serviceBrand: undefined; + + confirm(_confirmation: IConfirmation): Promise { return Promise.resolve({ confirmed: false }); } + show(_severity: Severity, _message: string, _buttons: string[], _options?: IDialogOptions): Promise { return Promise.resolve({ choice: 0 }); } + about(): Promise { return Promise.resolve(); } +} diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index 4a9c4489708..87d90b14746 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -123,7 +123,7 @@ export interface IExtensionContributions { views?: { [location: string]: IView[] }; colors?: IColor[]; localizations?: ILocalization[]; - readonly webviewEditors?: readonly IWebviewEditor[]; + readonly customEditors?: readonly IWebviewEditor[]; readonly codeActions?: readonly ICodeActionContribution[]; } diff --git a/src/vs/platform/instantiation/common/instantiationService.ts b/src/vs/platform/instantiation/common/instantiationService.ts index 0b010882458..bb33f2e1195 100644 --- a/src/vs/platform/instantiation/common/instantiationService.ts +++ b/src/vs/platform/instantiation/common/instantiationService.ts @@ -157,7 +157,7 @@ export class InstantiationService implements IInstantiationService { graph.lookupOrInsertNode(item); // a weak but working heuristic for cycle checks - if (cycleCount++ > 150) { + if (cycleCount++ > 200) { throw new CyclicDependencyError(graph); } diff --git a/src/vs/platform/keybinding/common/abstractKeybindingService.ts b/src/vs/platform/keybinding/common/abstractKeybindingService.ts index 6440a346f59..1fce530772a 100644 --- a/src/vs/platform/keybinding/common/abstractKeybindingService.ts +++ b/src/vs/platform/keybinding/common/abstractKeybindingService.ts @@ -35,6 +35,10 @@ export abstract class AbstractKeybindingService extends Disposable implements IK private _currentChordChecker: IntervalTimer; private _currentChordStatusMessage: IDisposable | null; + public get inChordMode(): boolean { + return !!this._currentChord; + } + constructor( private _contextKeyService: IContextKeyService, protected _commandService: ICommandService, diff --git a/src/vs/platform/keybinding/common/keybinding.ts b/src/vs/platform/keybinding/common/keybinding.ts index e11bd846c43..17ab82b8184 100644 --- a/src/vs/platform/keybinding/common/keybinding.ts +++ b/src/vs/platform/keybinding/common/keybinding.ts @@ -50,6 +50,8 @@ export const IKeybindingService = createDecorator('keybindin export interface IKeybindingService { _serviceBrand: undefined; + readonly inChordMode: boolean; + onDidUpdateKeybindings: Event; /** diff --git a/src/vs/platform/keybinding/common/keybindingResolver.ts b/src/vs/platform/keybinding/common/keybindingResolver.ts index 2ae709887b3..439e42d9948 100644 --- a/src/vs/platform/keybinding/common/keybindingResolver.ts +++ b/src/vs/platform/keybinding/common/keybindingResolver.ts @@ -11,7 +11,10 @@ import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKe import { keys } from 'vs/base/common/map'; export interface IResolveResult { + /** Whether the resolved keybinding is entering a chord */ enterChord: boolean; + /** Whether the resolved keybinding is leaving (and executing) a chord */ + leaveChord: boolean; commandId: string | null; commandArgs: any; bubble: boolean; @@ -285,6 +288,7 @@ export class KeybindingResolver { if (currentChord === null && result.keypressParts.length > 1 && result.keypressParts[1] !== null) { return { enterChord: true, + leaveChord: false, commandId: null, commandArgs: null, bubble: false @@ -293,6 +297,7 @@ export class KeybindingResolver { return { enterChord: false, + leaveChord: result.keypressParts.length > 1, commandId: result.command, commandArgs: result.commandArgs, bubble: result.bubble diff --git a/src/vs/platform/keybinding/test/common/mockKeybindingService.ts b/src/vs/platform/keybinding/test/common/mockKeybindingService.ts index c2062a4e4b4..8fcb795eda0 100644 --- a/src/vs/platform/keybinding/test/common/mockKeybindingService.ts +++ b/src/vs/platform/keybinding/test/common/mockKeybindingService.ts @@ -71,6 +71,8 @@ export class MockContextKeyService implements IContextKeyService { export class MockKeybindingService implements IKeybindingService { public _serviceBrand: undefined; + public readonly inChordMode: boolean = false; + public get onDidUpdateKeybindings(): Event { return Event.None; } diff --git a/src/vs/platform/notification/common/notification.ts b/src/vs/platform/notification/common/notification.ts index aff1675b214..a81672c8311 100644 --- a/src/vs/platform/notification/common/notification.ts +++ b/src/vs/platform/notification/common/notification.ts @@ -101,6 +101,12 @@ export interface INotification extends INotificationProperties { * this usecase and much easier to use! */ actions?: INotificationActions; + + /** + * The initial set of progress properties for the notification. To update progress + * later on, access the `INotificationHandle.progress` property. + */ + progress?: INotificationProgressProperties; } export interface INotificationActions { @@ -119,6 +125,24 @@ export interface INotificationActions { secondary?: ReadonlyArray; } +export interface INotificationProgressProperties { + + /** + * Causes the progress bar to spin infinitley. + */ + infinite?: boolean; + + /** + * Indicate the total amount of work. + */ + total?: number; + + /** + * Indicate that a specific chunk of work is done. + */ + worked?: number; +} + export interface INotificationProgress { /** diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts index 804d1138561..da88376513b 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts @@ -21,7 +21,7 @@ if (isWeb) { // Running out of sources if (Object.keys(product).length === 0) { assign(product, { - version: '1.41.0-dev', + version: '1.43.0-dev', nameLong: 'Visual Studio Code Web Dev', nameShort: 'VSCode Web Dev', urlProtocol: 'code-oss' diff --git a/src/vs/platform/product/common/productService.ts b/src/vs/platform/product/common/productService.ts index 23026219040..798f9f42518 100644 --- a/src/vs/platform/product/common/productService.ts +++ b/src/vs/platform/product/common/productService.ts @@ -103,6 +103,8 @@ export interface IProductConfiguration { readonly msftInternalDomains?: string[]; readonly linkProtectionTrustedDomains?: readonly string[]; + + readonly 'configurationSync.store'?: { url: string, authenticationProviderId: string }; } export interface IExeBasedExtensionTip { diff --git a/src/vs/platform/progress/common/progress.ts b/src/vs/platform/progress/common/progress.ts index 576ae6d82fb..a0cea0a3659 100644 --- a/src/vs/platform/progress/common/progress.ts +++ b/src/vs/platform/progress/common/progress.ts @@ -86,8 +86,6 @@ export interface IProgressRunner { done(): void; } -export const emptyProgress: IProgress = { report: () => { } }; - export const emptyProgressRunner: IProgressRunner = Object.freeze({ total() { }, worked() { }, @@ -100,6 +98,8 @@ export interface IProgress { export class Progress implements IProgress { + static readonly None: IProgress = Object.freeze({ report() { } }); + private _value?: T; get value(): T | undefined { return this._value; } diff --git a/src/vs/platform/undoRedo/common/undoRedo.ts b/src/vs/platform/undoRedo/common/undoRedo.ts index 75e08d09dcc..b8fef5dd2f6 100644 --- a/src/vs/platform/undoRedo/common/undoRedo.ts +++ b/src/vs/platform/undoRedo/common/undoRedo.ts @@ -8,42 +8,26 @@ import { URI } from 'vs/base/common/uri'; export const IUndoRedoService = createDecorator('undoRedoService'); -export interface IUndoRedoContext { - replaceCurrentElement(others: IUndoRedoElement[]): void; +export const enum UndoRedoElementType { + Resource, + Workspace } -export interface IUndoRedoElement { - /** - * None, one or multiple resources that this undo/redo element impacts. - */ - readonly resources: readonly URI[]; - - /** - * The label of the undo/redo element. - */ +export interface IResourceUndoRedoElement { + readonly type: UndoRedoElementType.Resource; + readonly resource: URI; readonly label: string; + undo(): Promise | void; + redo(): Promise | void; +} - /** - * Undo. - * Will always be called before `redo`. - * Can be called multiple times. - * e.g. `undo` -> `redo` -> `undo` -> `redo` - */ - undo(ctx: IUndoRedoContext): void; - - /** - * Redo. - * Will always be called after `undo`. - * Can be called multiple times. - * e.g. `undo` -> `redo` -> `undo` -> `redo` - */ - redo(ctx: IUndoRedoContext): void; - - /** - * Invalidate the edits concerning `resource`. - * i.e. the undo/redo stack for that particular resource has been destroyed. - */ - invalidate(resource: URI): void; +export interface IWorkspaceUndoRedoElement { + readonly type: UndoRedoElementType.Workspace; + readonly resources: readonly URI[]; + readonly label: string; + undo(): Promise | void; + redo(): Promise | void; + split(): IResourceUndoRedoElement[]; } export interface IUndoRedoService { @@ -53,12 +37,12 @@ export interface IUndoRedoService { * Add a new element to the `undo` stack. * This will destroy the `redo` stack. */ - pushElement(element: IUndoRedoElement): void; + pushElement(element: IResourceUndoRedoElement | IWorkspaceUndoRedoElement): void; /** * Get the last pushed element. If the last pushed element has been undone, returns null. */ - getLastElement(resource: URI): IUndoRedoElement | null; + getLastElement(resource: URI): IResourceUndoRedoElement | IWorkspaceUndoRedoElement | null; /** * Remove elements that target `resource`. @@ -66,8 +50,8 @@ export interface IUndoRedoService { removeElements(resource: URI): void; canUndo(resource: URI): boolean; - undo(resource: URI): void; + undo(resource: URI): Promise | void; - redo(resource: URI): void; canRedo(resource: URI): boolean; + redo(resource: URI): Promise | void; } diff --git a/src/vs/platform/undoRedo/common/undoRedoService.ts b/src/vs/platform/undoRedo/common/undoRedoService.ts index dfa7fb4cea2..ccbb2c55482 100644 --- a/src/vs/platform/undoRedo/common/undoRedoService.ts +++ b/src/vs/platform/undoRedo/common/undoRedoService.ts @@ -3,31 +3,88 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUndoRedoService, IUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo'; +import * as nls from 'vs/nls'; +import { IUndoRedoService, IResourceUndoRedoElement, IWorkspaceUndoRedoElement, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo'; import { URI } from 'vs/base/common/uri'; import { getComparisonKey as uriGetComparisonKey } from 'vs/base/common/resources'; import { onUnexpectedError } from 'vs/base/common/errors'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import Severity from 'vs/base/common/severity'; +import { Schemas } from 'vs/base/common/network'; +import { INotificationService } from 'vs/platform/notification/common/notification'; -class StackElement { - public readonly actual: IUndoRedoElement; +class ResourceStackElement { + public readonly type = UndoRedoElementType.Resource; + public readonly actual: IResourceUndoRedoElement; public readonly label: string; - public readonly resources: readonly URI[]; + + public readonly resource: URI; + public readonly strResource: string; + public readonly resources: URI[]; public readonly strResources: string[]; - constructor(actual: IUndoRedoElement) { + constructor(actual: IResourceUndoRedoElement) { this.actual = actual; this.label = actual.label; - this.resources = actual.resources; + this.resource = actual.resource; + this.strResource = uriGetComparisonKey(this.resource); + this.resources = [this.resource]; + this.strResources = [this.strResource]; + } +} + +const enum RemovedResourceReason { + ExternalRemoval = 0, + NoParallelUniverses = 1 +} + +class RemovedResources { + public readonly set: Set = new Set(); + public readonly reason: [URI[], URI[]] = [[], []]; + + public createMessage(): string { + let messages: string[] = []; + if (this.reason[RemovedResourceReason.ExternalRemoval].length > 0) { + const paths = this.reason[RemovedResourceReason.ExternalRemoval].map(uri => uri.scheme === Schemas.file ? uri.fsPath : uri.path); + messages.push(nls.localize('externalRemoval', "The following files have been closed: {0}.", paths.join(', '))); + } + if (this.reason[RemovedResourceReason.NoParallelUniverses].length > 0) { + const paths = this.reason[RemovedResourceReason.NoParallelUniverses].map(uri => uri.scheme === Schemas.file ? uri.fsPath : uri.path); + messages.push(nls.localize('noParallelUniverses', "The following files have been modified in an incompatible way: {0}.", paths.join(', '))); + } + return messages.join('\n'); + } +} + +class WorkspaceStackElement { + public readonly type = UndoRedoElementType.Workspace; + public readonly actual: IWorkspaceUndoRedoElement; + public readonly label: string; + + public readonly resources: URI[]; + public readonly strResources: string[]; + public removedResources: RemovedResources | null; + + constructor(actual: IWorkspaceUndoRedoElement) { + this.actual = actual; + this.label = actual.label; + this.resources = actual.resources.slice(0); this.strResources = this.resources.map(resource => uriGetComparisonKey(resource)); + this.removedResources = null; } - public invalidate(resource: URI): void { - if (this.resources.length > 1) { - this.actual.invalidate(resource); + public removeResource(resource: URI, strResource: string, reason: RemovedResourceReason): void { + if (!this.removedResources) { + this.removedResources = new RemovedResources(); + } + if (!this.removedResources.set.has(strResource)) { + this.removedResources.set.add(strResource); + this.removedResources.reason[reason].push(resource); } } } +type StackElement = ResourceStackElement | WorkspaceStackElement; class ResourceEditStack { public resource: URI; @@ -46,12 +103,15 @@ export class UndoRedoService implements IUndoRedoService { private readonly _editStacks: Map; - constructor() { + constructor( + @IDialogService private readonly _dialogService: IDialogService, + @INotificationService private readonly _notificationService: INotificationService, + ) { this._editStacks = new Map(); } - public pushElement(_element: IUndoRedoElement): void { - const element = new StackElement(_element); + public pushElement(_element: IResourceUndoRedoElement | IWorkspaceUndoRedoElement): void { + const element: StackElement = (_element.type === UndoRedoElementType.Resource ? new ResourceStackElement(_element) : new WorkspaceStackElement(_element)); for (let i = 0, len = element.resources.length; i < len; i++) { const resource = element.resources[i]; const strResource = element.strResources[i]; @@ -66,14 +126,16 @@ export class UndoRedoService implements IUndoRedoService { // remove the future for (const futureElement of editStack.future) { - futureElement.invalidate(resource); + if (futureElement.type === UndoRedoElementType.Workspace) { + futureElement.removeResource(resource, strResource, RemovedResourceReason.NoParallelUniverses); + } } editStack.future = []; editStack.past.push(element); } } - public getLastElement(resource: URI): IUndoRedoElement | null { + public getLastElement(resource: URI): IResourceUndoRedoElement | IWorkspaceUndoRedoElement | null { const strResource = uriGetComparisonKey(resource); if (this._editStacks.has(strResource)) { const editStack = this._editStacks.get(strResource)!; @@ -88,15 +150,75 @@ export class UndoRedoService implements IUndoRedoService { return null; } + private _splitPastWorkspaceElement(toRemove: WorkspaceStackElement, ignoreResources: Set | null): void { + const individualArr = toRemove.actual.split(); + const individualMap = new Map(); + for (const _element of individualArr) { + const element = new ResourceStackElement(_element); + individualMap.set(element.strResource, element); + } + + for (const strResource of toRemove.strResources) { + if (ignoreResources && ignoreResources.has(strResource)) { + continue; + } + const editStack = this._editStacks.get(strResource)!; + for (let j = editStack.past.length - 1; j >= 0; j--) { + if (editStack.past[j] === toRemove) { + if (individualMap.has(strResource)) { + // gets replaced + editStack.past[j] = individualMap.get(strResource)!; + } else { + // gets deleted + editStack.past.splice(j, 1); + } + break; + } + } + } + } + + private _splitFutureWorkspaceElement(toRemove: WorkspaceStackElement, ignoreResources: Set | null): void { + const individualArr = toRemove.actual.split(); + const individualMap = new Map(); + for (const _element of individualArr) { + const element = new ResourceStackElement(_element); + individualMap.set(element.strResource, element); + } + + for (const strResource of toRemove.strResources) { + if (ignoreResources && ignoreResources.has(strResource)) { + continue; + } + const editStack = this._editStacks.get(strResource)!; + for (let j = editStack.future.length - 1; j >= 0; j--) { + if (editStack.future[j] === toRemove) { + if (individualMap.has(strResource)) { + // gets replaced + editStack.future[j] = individualMap.get(strResource)!; + } else { + // gets deleted + editStack.future.splice(j, 1); + } + break; + } + } + } + } + public removeElements(resource: URI): void { const strResource = uriGetComparisonKey(resource); if (this._editStacks.has(strResource)) { const editStack = this._editStacks.get(strResource)!; - for (const pastElement of editStack.past) { - pastElement.invalidate(resource); + for (const element of editStack.past) { + if (element.type === UndoRedoElementType.Workspace) { + element.removeResource(resource, strResource, RemovedResourceReason.ExternalRemoval); + } } - for (const futureElement of editStack.future) { - futureElement.invalidate(resource); + for (const element of editStack.future) { + if (element.type === UndoRedoElementType.Workspace) { + element.removeResource(resource, strResource, RemovedResourceReason.ExternalRemoval); + } } this._editStacks.delete(strResource); } @@ -111,7 +233,92 @@ export class UndoRedoService implements IUndoRedoService { return false; } - public undo(resource: URI): void { + private _onError(err: Error, element: StackElement): void { + onUnexpectedError(err); + // An error occured while undoing or redoing => drop the undo/redo stack for all affected resources + for (const resource of element.resources) { + this.removeElements(resource); + } + this._notificationService.error(err); + } + + private _safeInvoke(element: StackElement, invoke: () => Promise | void): Promise | void { + let result: Promise | void; + try { + result = invoke(); + } catch (err) { + return this._onError(err, element); + } + + if (result) { + return result.then(undefined, (err) => this._onError(err, element)); + } + } + + private _workspaceUndo(resource: URI, element: WorkspaceStackElement): Promise | void { + if (element.removedResources) { + this._splitPastWorkspaceElement(element, element.removedResources.set); + const message = nls.localize('cannotWorkspaceUndo', "Could not undo '{0}' across all files. {1}", element.label, element.removedResources.createMessage()); + this._notificationService.info(message); + return this.undo(resource); + } + + // this must be the last past element in all the impacted resources! + let affectedEditStacks: ResourceEditStack[] = []; + for (const strResource of element.strResources) { + affectedEditStacks.push(this._editStacks.get(strResource)!); + } + + let cannotUndoDueToResources: URI[] = []; + for (const editStack of affectedEditStacks) { + if (editStack.past.length === 0 || editStack.past[editStack.past.length - 1] !== element) { + cannotUndoDueToResources.push(editStack.resource); + } + } + + if (cannotUndoDueToResources.length > 0) { + this._splitPastWorkspaceElement(element, null); + const paths = cannotUndoDueToResources.map(r => r.scheme === Schemas.file ? r.fsPath : r.path); + const message = nls.localize('cannotWorkspaceUndoDueToChanges', "Could not undo '{0}' across all files because changes were made to {1}", element.label, paths.join(', ')); + this._notificationService.info(message); + return this.undo(resource); + } + + return this._dialogService.show( + Severity.Info, + nls.localize('confirmWorkspace', "Would you like to undo '{0}' across all files?", element.label), + [ + nls.localize('ok', "Undo in {0} files.", affectedEditStacks.length), + nls.localize('nok', "Undo this file."), + nls.localize('cancel', "Cancel"), + ], + { + cancelId: 2 + } + ).then((result) => { + if (result.choice === 2) { + // cancel + return; + } else if (result.choice === 0) { + for (const editStack of affectedEditStacks) { + editStack.past.pop(); + editStack.future.push(element); + } + return this._safeInvoke(element, () => element.actual.undo()); + } else { + this._splitPastWorkspaceElement(element, null); + return this.undo(resource); + } + }); + } + + private _resourceUndo(editStack: ResourceEditStack, element: ResourceStackElement): Promise | void { + editStack.past.pop(); + editStack.future.push(element); + return this._safeInvoke(element, () => element.actual.undo()); + } + + public undo(resource: URI): Promise | void { const strResource = uriGetComparisonKey(resource); if (!this._editStacks.has(strResource)) { return; @@ -123,51 +330,10 @@ export class UndoRedoService implements IUndoRedoService { } const element = editStack.past[editStack.past.length - 1]; - - let replaceCurrentElement: IUndoRedoElement[] | null = null as IUndoRedoElement[] | null; - try { - element.actual.undo({ - replaceCurrentElement: (others: IUndoRedoElement[]): void => { - replaceCurrentElement = others; - } - }); - } catch (e) { - onUnexpectedError(e); - editStack.past.pop(); - editStack.future.push(element); - return; - } - - if (replaceCurrentElement === null) { - // regular case - editStack.past.pop(); - editStack.future.push(element); - return; - } - - const replaceCurrentElementMap = new Map(); - for (const _replace of replaceCurrentElement) { - const replace = new StackElement(_replace); - for (const strResource of replace.strResources) { - replaceCurrentElementMap.set(strResource, replace); - } - } - - for (let i = 0, len = element.strResources.length; i < len; i++) { - const strResource = element.strResources[i]; - if (this._editStacks.has(strResource)) { - const editStack = this._editStacks.get(strResource)!; - for (let j = editStack.past.length - 1; j >= 0; j--) { - if (editStack.past[j] === element) { - if (replaceCurrentElementMap.has(strResource)) { - editStack.past[j] = replaceCurrentElementMap.get(strResource)!; - } else { - editStack.past.splice(j, 1); - } - break; - } - } - } + if (element.type === UndoRedoElementType.Workspace) { + return this._workspaceUndo(resource, element); + } else { + return this._resourceUndo(editStack, element); } } @@ -180,7 +346,49 @@ export class UndoRedoService implements IUndoRedoService { return false; } - public redo(resource: URI): void { + private _workspaceRedo(resource: URI, element: WorkspaceStackElement): Promise | void { + if (element.removedResources) { + this._splitFutureWorkspaceElement(element, element.removedResources.set); + const message = nls.localize('cannotWorkspaceRedo', "Could not redo '{0}' across all files. {1}", element.label, element.removedResources.createMessage()); + this._notificationService.info(message); + return this.redo(resource); + } + + // this must be the last future element in all the impacted resources! + let affectedEditStacks: ResourceEditStack[] = []; + for (const strResource of element.strResources) { + affectedEditStacks.push(this._editStacks.get(strResource)!); + } + + let cannotRedoDueToResources: URI[] = []; + for (const editStack of affectedEditStacks) { + if (editStack.future.length === 0 || editStack.future[editStack.future.length - 1] !== element) { + cannotRedoDueToResources.push(editStack.resource); + } + } + + if (cannotRedoDueToResources.length > 0) { + this._splitFutureWorkspaceElement(element, null); + const paths = cannotRedoDueToResources.map(r => r.scheme === Schemas.file ? r.fsPath : r.path); + const message = nls.localize('cannotWorkspaceRedoDueToChanges', "Could not redo '{0}' across all files because changes were made to {1}", element.label, paths.join(', ')); + this._notificationService.info(message); + return this.redo(resource); + } + + for (const editStack of affectedEditStacks) { + editStack.future.pop(); + editStack.past.push(element); + } + return this._safeInvoke(element, () => element.actual.redo()); + } + + private _resourceRedo(editStack: ResourceEditStack, element: ResourceStackElement): Promise | void { + editStack.future.pop(); + editStack.past.push(element); + return this._safeInvoke(element, () => element.actual.redo()); + } + + public redo(resource: URI): Promise | void { const strResource = uriGetComparisonKey(resource); if (!this._editStacks.has(strResource)) { return; @@ -192,51 +400,10 @@ export class UndoRedoService implements IUndoRedoService { } const element = editStack.future[editStack.future.length - 1]; - - let replaceCurrentElement: IUndoRedoElement[] | null = null as IUndoRedoElement[] | null; - try { - element.actual.redo({ - replaceCurrentElement: (others: IUndoRedoElement[]): void => { - replaceCurrentElement = others; - } - }); - } catch (e) { - onUnexpectedError(e); - editStack.future.pop(); - editStack.past.push(element); - return; - } - - if (replaceCurrentElement === null) { - // regular case - editStack.future.pop(); - editStack.past.push(element); - return; - } - - const replaceCurrentElementMap = new Map(); - for (const _replace of replaceCurrentElement) { - const replace = new StackElement(_replace); - for (const strResource of replace.strResources) { - replaceCurrentElementMap.set(strResource, replace); - } - } - - for (let i = 0, len = element.strResources.length; i < len; i++) { - const strResource = element.strResources[i]; - if (this._editStacks.has(strResource)) { - const editStack = this._editStacks.get(strResource)!; - for (let j = editStack.future.length - 1; j >= 0; j--) { - if (editStack.future[j] === element) { - if (replaceCurrentElementMap.has(strResource)) { - editStack.future[j] = replaceCurrentElementMap.get(strResource)!; - } else { - editStack.future.splice(j, 1); - } - break; - } - } - } + if (element.type === UndoRedoElementType.Workspace) { + return this._workspaceRedo(resource, element); + } else { + return this._resourceRedo(editStack, element); } } } diff --git a/src/vs/platform/url/electron-main/electronUrlListener.ts b/src/vs/platform/url/electron-main/electronUrlListener.ts index 6c979560016..2523ccb0c5f 100644 --- a/src/vs/platform/url/electron-main/electronUrlListener.ts +++ b/src/vs/platform/url/electron-main/electronUrlListener.ts @@ -12,7 +12,6 @@ import { URI } from 'vs/base/common/uri'; import { IDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; import { isWindows } from 'vs/base/common/platform'; -import { coalesce } from 'vs/base/common/arrays'; import { disposableTimeout } from 'vs/base/common/async'; function uriFromRawUrl(url: string): URI | null { @@ -23,6 +22,16 @@ function uriFromRawUrl(url: string): URI | null { } } +/** + * A listener for URLs that are opened from the OS and handled by VSCode. + * Depending on the platform, this works differently: + * - Windows: we use `app.setAsDefaultProtocolClient()` to register VSCode with the OS + * and additionally add the `open-url` command line argument to identify. + * - macOS: we rely on `app.on('open-url')` to be called by the OS + * - Linux: we have a special shortcut installed (`resources/linux/code-url-handler.desktop`) + * that calls VSCode with the `open-url` command line argument + * (https://github.com/microsoft/vscode/pull/56727) + */ export class ElectronURLListener { private uris: URI[] = []; @@ -31,36 +40,34 @@ export class ElectronURLListener { private disposables = new DisposableStore(); constructor( - initial: string | string[], - @IURLService private readonly urlService: IURLService, - @IWindowsMainService windowsMainService: IWindowsMainService, - @IEnvironmentService environmentService: IEnvironmentService + initialUrisToHandle: URI[], + private readonly urlService: IURLService, + windowsMainService: IWindowsMainService, + environmentService: IEnvironmentService ) { - const globalBuffer = ((global).getOpenUrls() || []) as string[]; - const rawBuffer = [ - ...(typeof initial === 'string' ? [initial] : initial), - ...globalBuffer - ]; - this.uris = coalesce(rawBuffer.map(uriFromRawUrl)); + // the initial set of URIs we need to handle once the window is ready + this.uris = initialUrisToHandle; + // Windows: install as protocol handler if (isWindows) { const windowsParameters = environmentService.isBuilt ? [] : [`"${environmentService.appRoot}"`]; windowsParameters.push('--open-url', '--'); app.setAsDefaultProtocolClient(product.urlProtocol, process.execPath, windowsParameters); } + // macOS: listen to `open-url` events from here on to handle const onOpenElectronUrl = Event.map( Event.fromNodeEventEmitter(app, 'open-url', (event: ElectronEvent, url: string) => ({ event, url })), ({ event, url }) => { - // always prevent default and return the url as string - event.preventDefault(); + event.preventDefault(); // always prevent default and return the url as string return url; }); const onOpenUrl = Event.filter(Event.map(onOpenElectronUrl, uriFromRawUrl), (uri): uri is URI => !!uri); onOpenUrl(this.urlService.open, this.urlService, this.disposables); + // Send initial links to the window once it has loaded const isWindowReady = windowsMainService.getWindows() .filter(w => w.isReady) .length > 0; diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index cdf2baeef81..92590672d67 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -18,8 +18,7 @@ import { ParseError, parse } from 'vs/base/common/json'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; import { IStringDictionary } from 'vs/base/common/collections'; import { localize } from 'vs/nls'; - -const BACK_UP_MAX_AGE = 1000 * 60 * 60 * 24 * 30; /* 30 days */ +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; type SyncSourceClassification = { source?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; @@ -65,6 +64,7 @@ export abstract class AbstractSynchroniser extends Disposable { @IUserDataSyncEnablementService protected readonly userDataSyncEnablementService: IUserDataSyncEnablementService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IUserDataSyncLogService protected readonly logService: IUserDataSyncLogService, + @IConfigurationService protected readonly configurationService: IConfigurationService, ) { super(); this.syncFolder = joinPath(environmentService.userDataSyncHome, source); @@ -91,7 +91,7 @@ export abstract class AbstractSynchroniser extends Disposable { protected get enabled(): boolean { return this.userDataSyncEnablementService.isResourceEnabled(this.resourceKey); } - async sync(ref?: string): Promise { + async sync(ref?: string, donotUseLastSyncUserData?: boolean): Promise { if (!this.enabled) { this.logService.info(`${this.source}: Skipped synchronizing ${this.source.toLowerCase()} as it is disabled.`); return; @@ -108,7 +108,7 @@ export abstract class AbstractSynchroniser extends Disposable { this.logService.trace(`${this.source}: Started synchronizing ${this.source.toLowerCase()}...`); this.setStatus(SyncStatus.Syncing); - const lastSyncUserData = await this.getLastSyncUserData(); + const lastSyncUserData = donotUseLastSyncUserData ? null : await this.getLastSyncUserData(); const remoteUserData = ref && lastSyncUserData && lastSyncUserData.ref === ref ? lastSyncUserData : await this.getRemoteUserData(lastSyncUserData); if (remoteUserData.syncData && remoteUserData.syncData.version > this.version) { @@ -117,7 +117,19 @@ export abstract class AbstractSynchroniser extends Disposable { throw new UserDataSyncError(localize('incompatible', "Cannot sync {0} as its version {1} is not compatible with cloud {2}", this.source, this.version, remoteUserData.syncData.version), UserDataSyncErrorCode.Incompatible, this.source); } - return this.doSync(remoteUserData, lastSyncUserData); + try { + await this.doSync(remoteUserData, lastSyncUserData); + } catch (e) { + if (e instanceof UserDataSyncError) { + switch (e.code) { + case UserDataSyncErrorCode.RemotePreconditionFailed: + // Rejected as there is a new remote version. Syncing again, + this.logService.info(`${this.source}: Failed to synchronize as there is a new remote version available. Synchronizing again...`); + return this.sync(undefined, true); + } + } + throw e; + } } async hasPreviouslySynced(): Promise { @@ -190,17 +202,26 @@ export abstract class AbstractSynchroniser extends Disposable { } protected async backupLocal(content: VSBuffer): Promise { - const resource = joinPath(this.syncFolder, toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')); - await this.fileService.writeFile(resource, content); + const resource = joinPath(this.syncFolder, `${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}.json`); + try { + await this.fileService.writeFile(resource, content); + } catch (e) { + this.logService.error(e); + } this.cleanUpDelayer.trigger(() => this.cleanUpBackup()); } private async cleanUpBackup(): Promise { - const stat = await this.fileService.resolve(this.syncFolder); - if (stat.children) { - const all = stat.children.filter(stat => stat.isFile && /^\d{8}T\d{6}$/.test(stat.name)); - if (all.length > 10) { - const toDelete = all.filter(stat => { + try { + if (!(await this.fileService.exists(this.syncFolder))) { + return; + } + const stat = await this.fileService.resolve(this.syncFolder); + if (stat.children) { + const all = stat.children.filter(stat => stat.isFile && /^\d{8}T\d{6}(\.json)?$/.test(stat.name)).sort(); + console.log(all.map(a => a.name)); + const backUpMaxAge = 1000 * 60 * 60 * 24 * (this.configurationService.getValue('sync.localBackupDuration') || 30 /* Default 30 days */); + let toDelete = all.filter(stat => { const ctime = stat.ctime || new Date( parseInt(stat.name.substring(0, 4)), parseInt(stat.name.substring(4, 6)) - 1, @@ -209,13 +230,19 @@ export abstract class AbstractSynchroniser extends Disposable { parseInt(stat.name.substring(11, 13)), parseInt(stat.name.substring(13, 15)) ).getTime(); - return Date.now() - ctime > BACK_UP_MAX_AGE; + return Date.now() - ctime > backUpMaxAge; }); + const remaining = all.length - toDelete.length; + if (remaining < 10) { + toDelete = toDelete.slice(10 - remaining); + } await Promise.all(toDelete.map(stat => { this.logService.info('Deleting from backup', stat.resource.path); this.fileService.del(stat.resource); })); } + } catch (e) { + this.logService.error(e); } } @@ -247,8 +274,9 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser { @IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService, @ITelemetryService telemetryService: ITelemetryService, @IUserDataSyncLogService logService: IUserDataSyncLogService, + @IConfigurationService configurationService: IConfigurationService, ) { - super(source, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService); + super(source, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); this._register(this.fileService.watch(dirname(file))); this._register(this.fileService.onDidFilesChange(e => this.onFileChanges(e))); } @@ -346,8 +374,9 @@ export abstract class AbstractJsonFileSynchroniser extends AbstractFileSynchroni @ITelemetryService telemetryService: ITelemetryService, @IUserDataSyncLogService logService: IUserDataSyncLogService, @IUserDataSyncUtilService protected readonly userDataSyncUtilService: IUserDataSyncUtilService, + @IConfigurationService configurationService: IConfigurationService, ) { - super(file, source, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService); + super(file, source, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); } protected hasErrors(content: string): boolean { diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index 0c189d343c9..ccdf3c48517 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncSource, ResourceKey, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { SyncStatus, IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncSource, ResourceKey, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { Event } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement'; @@ -46,11 +46,11 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse @IGlobalExtensionEnablementService private readonly extensionEnablementService: IGlobalExtensionEnablementService, @IUserDataSyncLogService logService: IUserDataSyncLogService, @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, - @IConfigurationService private readonly configurationService: IConfigurationService, + @IConfigurationService configurationService: IConfigurationService, @IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService, @ITelemetryService telemetryService: ITelemetryService, ) { - super(SyncSource.Extensions, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService); + super(SyncSource.Extensions, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); this._register( Event.debounce( Event.any( @@ -154,11 +154,6 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse await this.apply(previewResult); } catch (e) { this.setStatus(SyncStatus.Idle); - if (e instanceof UserDataSyncError && e.code === UserDataSyncErrorCode.RemotePreconditionFailed) { - // Rejected as there is a new remote version. Syncing again, - this.logService.info('Extensions: Failed to synchronize extensions as there is a new remote version available. Synchronizing again...'); - return this.sync(); - } throw e; } @@ -199,7 +194,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse if (added.length || removed.length || updated.length) { // back up all disabled or market place extensions const backUpExtensions = localExtensions.filter(e => e.disabled || !!e.identifier.uuid); - await this.backupLocal(VSBuffer.fromString(JSON.stringify(backUpExtensions))); + await this.backupLocal(VSBuffer.fromString(JSON.stringify(backUpExtensions, null, '\t'))); skippedExtensions = await this.updateLocalExtensions(added, removed, updated, skippedExtensions); } diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts index 6e76d260dc2..8e00f07a9ae 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncSource, IUserDataSynchroniser, ResourceKey, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncSource, IUserDataSynchroniser, ResourceKey, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { VSBuffer } from 'vs/base/common/buffer'; import { Event } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -15,6 +15,7 @@ import { merge } from 'vs/platform/userDataSync/common/globalStateMerge'; import { parse } from 'vs/base/common/json'; import { AbstractSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; const argvProperties: string[] = ['locale']; @@ -38,8 +39,9 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs @IEnvironmentService private readonly environmentService: IEnvironmentService, @IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService, @ITelemetryService telemetryService: ITelemetryService, + @IConfigurationService configurationService: IConfigurationService, ) { - super(SyncSource.GlobalState, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService); + super(SyncSource.GlobalState, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); this._register(this.fileService.watch(dirname(this.environmentService.argvResource))); this._register(Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.environmentService.argvResource))(() => this._onDidChangeLocal.fire())); } @@ -129,11 +131,6 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs this.logService.trace('UI State: Finished synchronizing ui state.'); } catch (e) { this.setStatus(SyncStatus.Idle); - if (e instanceof UserDataSyncError && e.code === UserDataSyncErrorCode.RemotePreconditionFailed) { - // Rejected as there is a new remote version. Syncing again, - this.logService.info('UI State: Failed to synchronize ui state as there is a new remote version available. Synchronizing again...'); - return this.sync(); - } throw e; } finally { this.setStatus(SyncStatus.Idle); @@ -168,7 +165,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs if (local) { // update local this.logService.trace('UI State: Updating local ui state...'); - await this.backupLocal(VSBuffer.fromString(JSON.stringify(localUserData))); + await this.backupLocal(VSBuffer.fromString(JSON.stringify(localUserData, null, '\t'))); await this.writeLocalGlobalState(local); this.logService.info('UI State: Updated local ui state'); } diff --git a/src/vs/platform/userDataSync/common/keybindingsSync.ts b/src/vs/platform/userDataSync/common/keybindingsSync.ts index 251999ef64d..8d1d78409fd 100644 --- a/src/vs/platform/userDataSync/common/keybindingsSync.ts +++ b/src/vs/platform/userDataSync/common/keybindingsSync.ts @@ -36,14 +36,14 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem constructor( @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncLogService logService: IUserDataSyncLogService, - @IConfigurationService private readonly configurationService: IConfigurationService, + @IConfigurationService configurationService: IConfigurationService, @IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService, @IFileService fileService: IFileService, @IEnvironmentService private readonly environmentService: IEnvironmentService, @IUserDataSyncUtilService userDataSyncUtilService: IUserDataSyncUtilService, @ITelemetryService telemetryService: ITelemetryService, ) { - super(environmentService.keybindingsResource, SyncSource.Keybindings, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService, userDataSyncUtilService); + super(environmentService.keybindingsResource, SyncSource.Keybindings, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService); } async pull(): Promise { @@ -180,10 +180,6 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem this.setStatus(SyncStatus.Idle); if (e instanceof UserDataSyncError) { switch (e.code) { - case UserDataSyncErrorCode.RemotePreconditionFailed: - // Rejected as there is a new remote version. Syncing again, - this.logService.info('Keybindings: Failed to synchronize keybindings as there is a new remote version available. Synchronizing again...'); - return this.sync(); case UserDataSyncErrorCode.LocalPreconditionFailed: // Rejected as there is a new local version. Syncing again. this.logService.info('Keybindings: Failed to synchronize keybindings as there is a new local version available. Synchronizing again...'); diff --git a/src/vs/platform/userDataSync/common/settingsMerge.ts b/src/vs/platform/userDataSync/common/settingsMerge.ts index 4942add903f..cd74d415f26 100644 --- a/src/vs/platform/userDataSync/common/settingsMerge.ts +++ b/src/vs/platform/userDataSync/common/settingsMerge.ts @@ -10,8 +10,8 @@ import { values } from 'vs/base/common/map'; import { IStringDictionary } from 'vs/base/common/collections'; import { FormattingOptions, Edit, getEOL } from 'vs/base/common/jsonFormatter'; import * as contentUtil from 'vs/platform/userDataSync/common/content'; -import { IConflictSetting, CONFIGURATION_SYNC_STORE_KEY } from 'vs/platform/userDataSync/common/userDataSync'; -import { firstIndex } from 'vs/base/common/arrays'; +import { IConflictSetting, getDisallowedIgnoredSettings } from 'vs/platform/userDataSync/common/userDataSync'; +import { firstIndex, distinct } from 'vs/base/common/arrays'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { startsWith } from 'vs/base/common/strings'; @@ -22,7 +22,7 @@ export interface IMergeResult { conflictsSettings: IConflictSetting[]; } -export function getIgnoredSettings(configurationService: IConfigurationService, settingsContent?: string): string[] { +export function getIgnoredSettings(defaultIgnoredSettings: string[], configurationService: IConfigurationService, settingsContent?: string): string[] { let value: string[] = []; if (settingsContent) { const setting = parse(settingsContent); @@ -32,7 +32,7 @@ export function getIgnoredSettings(configurationService: IConfigurationService, } else { value = configurationService.getValue('sync.ignoredSettings'); } - const added: string[] = [], removed: string[] = []; + const added: string[] = [], removed: string[] = [...getDisallowedIgnoredSettings()]; if (Array.isArray(value)) { for (const key of value) { if (startsWith(key, '-')) { @@ -42,7 +42,7 @@ export function getIgnoredSettings(configurationService: IConfigurationService, } } } - return [CONFIGURATION_SYNC_STORE_KEY, ...added].filter(setting => removed.indexOf(setting) === -1); + return distinct([...defaultIgnoredSettings, ...added,].filter(setting => removed.indexOf(setting) === -1)); } diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index abb2996f999..4dcce8bb0ee 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -21,6 +21,7 @@ import { edit } from 'vs/platform/userDataSync/common/content'; import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { URI } from 'vs/base/common/uri'; +import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; interface ISettingsSyncContent { settings: string; @@ -51,11 +52,12 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncLogService logService: IUserDataSyncLogService, @IUserDataSyncUtilService userDataSyncUtilService: IUserDataSyncUtilService, - @IConfigurationService private readonly configurationService: IConfigurationService, + @IConfigurationService configurationService: IConfigurationService, @IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService, @ITelemetryService telemetryService: ITelemetryService, + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, ) { - super(environmentService.settingsResource, SyncSource.Settings, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService, userDataSyncUtilService); + super(environmentService.settingsResource, SyncSource.Settings, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService); } protected setStatus(status: SyncStatus): void { @@ -94,7 +96,8 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement const fileContent = await this.getLocalFileContent(); const formatUtils = await this.getFormattingOptions(); // Update ignored settings from local file content - const content = updateIgnoredSettings(remoteSettingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', getIgnoredSettings(this.configurationService), formatUtils); + const ignoredSettings = await this.getIgnoredSettings(); + const content = updateIgnoredSettings(remoteSettingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils); this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve({ fileContent, remoteUserData, @@ -136,7 +139,8 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement if (fileContent !== null) { const formatUtils = await this.getFormattingOptions(); // Remove ignored settings - const content = updateIgnoredSettings(fileContent.value.toString(), '{}', getIgnoredSettings(this.configurationService), formatUtils); + const ignoredSettings = await this.getIgnoredSettings(); + const content = updateIgnoredSettings(fileContent.value.toString(), '{}', ignoredSettings, formatUtils); const lastSyncUserData = await this.getLastSyncUserData(); const remoteUserData = await this.getRemoteUserData(lastSyncUserData); @@ -192,7 +196,8 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement if (preview && content !== null) { const formatUtils = await this.getFormattingOptions(); // remove ignored settings from the remote content for preview - content = updateIgnoredSettings(content, '{}', getIgnoredSettings(this.configurationService), formatUtils); + const ignoredSettings = await this.getIgnoredSettings(); + content = updateIgnoredSettings(content, '{}', ignoredSettings, formatUtils); } return content; } @@ -203,7 +208,8 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement this.cancel(); const formatUtils = await this.getFormattingOptions(); // Add ignored settings from local file content - content = updateIgnoredSettings(content, preview.fileContent ? preview.fileContent.value.toString() : '{}', getIgnoredSettings(this.configurationService), formatUtils); + const ignoredSettings = await this.getIgnoredSettings(); + content = updateIgnoredSettings(content, preview.fileContent ? preview.fileContent.value.toString() : '{}', ignoredSettings, formatUtils); this.syncPreviewResultPromise = createCancelablePromise(async () => ({ ...preview, content })); await this.apply(true); this.setStatus(SyncStatus.Idle); @@ -237,10 +243,6 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement this.setStatus(SyncStatus.Idle); if (e instanceof UserDataSyncError) { switch (e.code) { - case UserDataSyncErrorCode.RemotePreconditionFailed: - // Rejected as there is a new remote version. Syncing again, - this.logService.info('Settings: Failed to synchronize settings as there is a new remote version available. Synchronizing again...'); - return this.sync(); case UserDataSyncErrorCode.LocalPreconditionFailed: // Rejected as there is a new local version. Syncing again. this.logService.info('Settings: Failed to synchronize settings as there is a new local version available. Synchronizing again...'); @@ -271,7 +273,8 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement const formatUtils = await this.getFormattingOptions(); // Update ignored settings from remote const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData); - content = updateIgnoredSettings(content, remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : '{}', getIgnoredSettings(this.configurationService, content), formatUtils); + const ignoredSettings = await this.getIgnoredSettings(content); + content = updateIgnoredSettings(content, remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : '{}', ignoredSettings, formatUtils); this.logService.trace('Settings: Updating remote settings...'); remoteUserData = await this.updateRemoteUserData(JSON.stringify({ settings: content }), forcePush ? null : remoteUserData.ref); this.logService.info('Settings: Updated remote settings'); @@ -317,7 +320,8 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement const localContent: string = fileContent ? fileContent.value.toString() : '{}'; this.validateContent(localContent); this.logService.trace('Settings: Merging remote settings with local settings...'); - const result = merge(localContent, remoteSettingsSyncContent.settings, lastSettingsSyncContent ? lastSettingsSyncContent.settings : null, getIgnoredSettings(this.configurationService), resolvedConflicts, formattingOptions); + const ignoredSettings = await this.getIgnoredSettings(); + const result = merge(localContent, remoteSettingsSyncContent.settings, lastSettingsSyncContent ? lastSettingsSyncContent.settings : null, ignoredSettings, resolvedConflicts, formattingOptions); content = result.localContent || result.remoteContent; hasLocalChanged = result.localContent !== null; hasRemoteChanged = result.remoteContent !== null; @@ -334,7 +338,8 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement if (content && !token.isCancellationRequested) { // Remove the ignored settings from the preview. - const previewContent = updateIgnoredSettings(content, '{}', getIgnoredSettings(this.configurationService), formattingOptions); + const ignoredSettings = await this.getIgnoredSettings(); + const previewContent = updateIgnoredSettings(content, '{}', ignoredSettings, formattingOptions); await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, VSBuffer.fromString(previewContent)); } @@ -356,6 +361,21 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement return null; } + private _defaultIgnoredSettings: Promise | undefined = undefined; + protected async getIgnoredSettings(content?: string): Promise { + if (!this._defaultIgnoredSettings) { + this._defaultIgnoredSettings = this.userDataSyncUtilService.resolveDefaultIgnoredSettings(); + const disposable = Event.any( + Event.filter(this.extensionManagementService.onDidInstallExtension, (e => !!e.gallery)), + Event.filter(this.extensionManagementService.onDidUninstallExtension, (e => !e.error)))(() => { + disposable.dispose(); + this._defaultIgnoredSettings = undefined; + }); + } + const defaultIgnoredSettings = await this._defaultIgnoredSettings; + return getIgnoredSettings(defaultIgnoredSettings, this.configurationService, content); + } + private validateContent(content: string): void { if (this.hasErrors(content)) { throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync settings as there are errors/warning in settings file."), UserDataSyncErrorCode.LocalInvalidContent, this.source); diff --git a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts index b64192c62d6..b623b9754a0 100644 --- a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts @@ -8,6 +8,11 @@ import { Event, Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IUserDataSyncLogService, IUserDataSyncService, SyncStatus, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; + +type AutoSyncTriggerClassification = { + source: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; +}; export class UserDataAutoSyncService extends Disposable implements IUserDataAutoSyncService { @@ -25,6 +30,7 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, @IAuthenticationTokenService private readonly authTokenService: IAuthenticationTokenService, + @ITelemetryService private readonly telemetryService: ITelemetryService, ) { super(); this.updateEnablement(false, true); @@ -32,7 +38,7 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto this._register(Event.any(authTokenService.onDidChangeToken)(() => this.updateEnablement(true, true))); this._register(Event.any(userDataSyncService.onDidChangeStatus)(() => this.updateEnablement(true, true))); this._register(this.userDataSyncEnablementService.onDidChangeEnablement(() => this.updateEnablement(true, false))); - this._register(this.userDataSyncEnablementService.onDidChangeResourceEnablement(() => this.triggerAutoSync())); + this._register(this.userDataSyncEnablementService.onDidChangeResourceEnablement(() => this.triggerAutoSync(['resourceEnablement']))); } private async updateEnablement(stopIfDisabled: boolean, auto: boolean): Promise { @@ -99,7 +105,8 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto this.successiveFailures = 0; } - async triggerAutoSync(): Promise { + async triggerAutoSync(sources: string[]): Promise { + sources.forEach(source => this.telemetryService.publicLog2<{ source: string }, AutoSyncTriggerClassification>('sync/triggerAutoSync', { source })); if (this.enabled) { return this.syncDelayer.trigger(() => { this.logService.info('Auto Sync: Triggered.'); diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 240d6ff1334..79589c43e4f 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -20,6 +20,8 @@ import { FormattingOptions } from 'vs/base/common/jsonFormatter'; import { URI } from 'vs/base/common/uri'; import { isEqual, joinPath } from 'vs/base/common/resources'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { distinct } from 'vs/base/common/arrays'; export const CONFIGURATION_SYNC_STORE_KEY = 'configurationSync.store'; @@ -36,6 +38,18 @@ export interface ISyncConfiguration { } } +export function getDisallowedIgnoredSettings(): string[] { + const allSettings = Registry.as(ConfigurationExtensions.Configuration).getConfigurationProperties(); + return Object.keys(allSettings).filter(setting => !!allSettings[setting].disallowSyncIgnore); +} + +export function getDefaultIgnoredSettings(): string[] { + const allSettings = Registry.as(ConfigurationExtensions.Configuration).getConfigurationProperties(); + const machineSettings = Object.keys(allSettings).filter(setting => allSettings[setting].scope === ConfigurationScope.MACHINE || allSettings[setting].scope === ConfigurationScope.MACHINE_OVERRIDABLE); + const disallowedSettings = getDisallowedIgnoredSettings(); + return distinct([CONFIGURATION_SYNC_STORE_KEY, ...machineSettings, ...disallowedSettings]); +} + export function registerConfiguration(): IDisposable { const ignoredSettingsSchemaId = 'vscode://schemas/ignoredSettings'; const ignoredExtensionsSchemaId = 'vscode://schemas/ignoredExtensions'; @@ -46,41 +60,12 @@ export function registerConfiguration(): IDisposable { title: localize('sync', "Sync"), type: 'object', properties: { - 'sync.enable': { - type: 'boolean', - default: false, - scope: ConfigurationScope.APPLICATION, - deprecationMessage: 'deprecated' - }, - 'sync.enableSettings': { - type: 'boolean', - default: true, - scope: ConfigurationScope.APPLICATION, - deprecationMessage: 'deprecated' - }, - 'sync.enableKeybindings': { - type: 'boolean', - default: true, - scope: ConfigurationScope.APPLICATION, - deprecationMessage: 'Deprecated' - }, - 'sync.enableUIState': { - type: 'boolean', - default: true, - scope: ConfigurationScope.APPLICATION, - deprecationMessage: 'deprecated' - }, - 'sync.enableExtensions': { - type: 'boolean', - default: true, - scope: ConfigurationScope.APPLICATION, - deprecationMessage: 'deprecated' - }, 'sync.keybindingsPerPlatform': { type: 'boolean', description: localize('sync.keybindingsPerPlatform', "Synchronize keybindings per platform."), default: true, scope: ConfigurationScope.APPLICATION, + tags: ['sync'] }, 'sync.ignoredExtensions': { 'type': 'array', @@ -89,7 +74,8 @@ export function registerConfiguration(): IDisposable { 'default': [], 'scope': ConfigurationScope.APPLICATION, uniqueItems: true, - disallowSyncIgnore: true + disallowSyncIgnore: true, + tags: ['sync'] }, 'sync.ignoredSettings': { 'type': 'array', @@ -99,17 +85,23 @@ export function registerConfiguration(): IDisposable { $ref: ignoredSettingsSchemaId, additionalProperties: true, uniqueItems: true, - disallowSyncIgnore: true + disallowSyncIgnore: true, + tags: ['sync'] } } }); const jsonRegistry = Registry.as(JSONExtensions.JSONContribution); const registerIgnoredSettingsSchema = () => { + const disallowedIgnoredSettings = getDisallowedIgnoredSettings(); + const defaultIgnoredSettings = getDefaultIgnoredSettings().filter(s => s !== CONFIGURATION_SYNC_STORE_KEY); + const settings = Object.keys(allSettings.properties).filter(setting => defaultIgnoredSettings.indexOf(setting) === -1); + const ignoredSettings = defaultIgnoredSettings.filter(setting => disallowedIgnoredSettings.indexOf(setting) === -1); + console.log(ignoredSettings); const ignoredSettingsSchema: IJSONSchema = { items: { type: 'string', - enum: Object.keys(allSettings.properties) - } + enum: [...settings, ...ignoredSettings.map(setting => `-${setting}`)] + }, }; jsonRegistry.registerSchema(ignoredSettingsSchemaId, ignoredSettingsSchema); }; @@ -133,8 +125,8 @@ export interface IUserDataSyncStore { authenticationProviderId: string; } -export function getUserDataSyncStore(configurationService: IConfigurationService): IUserDataSyncStore | undefined { - const value = configurationService.getValue<{ url: string, authenticationProviderId: string }>(CONFIGURATION_SYNC_STORE_KEY); +export function getUserDataSyncStore(productService: IProductService, configurationService: IConfigurationService): IUserDataSyncStore | undefined { + const value = productService[CONFIGURATION_SYNC_STORE_KEY] || configurationService.getValue<{ url: string, authenticationProviderId: string }>(CONFIGURATION_SYNC_STORE_KEY); if (value && value.url && value.authenticationProviderId) { return { url: joinPath(URI.parse(value.url), 'v1'), @@ -180,6 +172,7 @@ export enum UserDataSyncErrorCode { // Local Errors LocalPreconditionFailed = 'LocalPreconditionFailed', LocalInvalidContent = 'LocalInvalidContent', + LocalError = 'LocalError', Incompatible = 'Incompatible', Unknown = 'Unknown', @@ -285,7 +278,8 @@ export interface IUserDataSyncService { readonly conflictsSources: SyncSource[]; readonly onDidChangeConflicts: Event; - readonly onDidChangeLocal: Event; + readonly onDidChangeLocal: Event; + readonly onSyncErrors: Event<[SyncSource, UserDataSyncError][]>; readonly lastSyncTime: number | undefined; readonly onDidChangeLastSyncTime: Event; @@ -305,7 +299,7 @@ export const IUserDataAutoSyncService = createDecorator; - triggerAutoSync(): Promise; + triggerAutoSync(sources: string[]): Promise; } export const IUserDataSyncUtilService = createDecorator('IUserDataSyncUtilService'); @@ -313,6 +307,7 @@ export interface IUserDataSyncUtilService { _serviceBrand: undefined; resolveUserBindings(userbindings: string[]): Promise>; resolveFormattingOptions(resource: URI): Promise; + resolveDefaultIgnoredSettings(): Promise; } export const IUserDataSyncLogService = createDecorator('IUserDataSyncLogService'); diff --git a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts index 32547f516f2..a2b8c25e64d 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts @@ -20,6 +20,7 @@ export class UserDataSyncChannel implements IServerChannel { case 'onDidChangeConflicts': return this.service.onDidChangeConflicts; case 'onDidChangeLocal': return this.service.onDidChangeLocal; case 'onDidChangeLastSyncTime': return this.service.onDidChangeLastSyncTime; + case 'onSyncErrors': return this.service.onSyncErrors; } throw new Error(`Event not found: ${event}`); } @@ -85,7 +86,7 @@ export class UserDataAutoSyncChannel implements IServerChannel { call(context: any, command: string, args?: any): Promise { switch (command) { - case 'triggerAutoSync': return this.service.triggerAutoSync(); + case 'triggerAutoSync': return this.service.triggerAutoSync(args[0]); } throw new Error('Invalid call'); } @@ -101,6 +102,7 @@ export class UserDataSycnUtilServiceChannel implements IServerChannel { call(context: any, command: string, args?: any): Promise { switch (command) { + case 'resolveDefaultIgnoredSettings': return this.service.resolveDefaultIgnoredSettings(); case 'resolveUserKeybindings': return this.service.resolveUserBindings(args[0]); case 'resolveFormattingOptions': return this.service.resolveFormattingOptions(URI.revive(args[0])); } @@ -115,6 +117,10 @@ export class UserDataSyncUtilServiceClient implements IUserDataSyncUtilService { constructor(private readonly channel: IChannel) { } + async resolveDefaultIgnoredSettings(): Promise { + return this.channel.call('resolveDefaultIgnoredSettings'); + } + async resolveUserBindings(userbindings: string[]): Promise> { return this.channel.call('resolveUserKeybindings', [userbindings]); } diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index 94e476f67f2..4d0a2619afe 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -34,13 +34,17 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ private _onDidChangeStatus: Emitter = this._register(new Emitter()); readonly onDidChangeStatus: Event = this._onDidChangeStatus.event; - readonly onDidChangeLocal: Event; + readonly onDidChangeLocal: Event; private _conflictsSources: SyncSource[] = []; get conflictsSources(): SyncSource[] { return this._conflictsSources; } private _onDidChangeConflicts: Emitter = this._register(new Emitter()); readonly onDidChangeConflicts: Event = this._onDidChangeConflicts.event; + private _syncErrors: [SyncSource, UserDataSyncError][] = []; + private _onSyncErrors: Emitter<[SyncSource, UserDataSyncError][]> = this._register(new Emitter<[SyncSource, UserDataSyncError][]>()); + readonly onSyncErrors: Event<[SyncSource, UserDataSyncError][]> = this._onSyncErrors.event; + private _lastSyncTime: number | undefined = undefined; get lastSyncTime(): number | undefined { return this._lastSyncTime; } private _onDidChangeLastSyncTime: Emitter = this._register(new Emitter()); @@ -70,7 +74,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } this._lastSyncTime = this.storageService.getNumber(LAST_SYNC_TIME_KEY, StorageScope.GLOBAL, undefined); - this.onDidChangeLocal = Event.any(...this.synchronisers.map(s => s.onDidChangeLocal)); + this.onDidChangeLocal = Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeLocal, () => s.source))); } async pull(): Promise { @@ -101,6 +105,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ await this.checkEnablement(); const startTime = new Date().getTime(); + this._syncErrors = []; try { this.logService.trace('Sync started.'); if (this.status !== SyncStatus.HasConflicts) { @@ -126,6 +131,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ await synchroniser.sync(manifest && manifest.latest ? manifest.latest[synchroniser.resourceKey] : undefined); } catch (e) { this.handleSyncError(e, synchroniser.source); + this._syncErrors.push([synchroniser.source, UserDataSyncError.toUserDataSyncError(e)]); } } @@ -144,6 +150,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } finally { this.updateStatus(); + this._onSyncErrors.fire(this._syncErrors); } } diff --git a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts index bc662099f33..2280e1acc28 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts @@ -11,6 +11,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IHeaders, IRequestOptions, IRequestContext } from 'vs/base/parts/request/common/request'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication'; +import { IProductService } from 'vs/platform/product/common/productService'; export class UserDataSyncStoreService extends Disposable implements IUserDataSyncStoreService { @@ -19,13 +20,14 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn readonly userDataSyncStore: IUserDataSyncStore | undefined; constructor( + @IProductService productService: IProductService, @IConfigurationService configurationService: IConfigurationService, @IRequestService private readonly requestService: IRequestService, @IAuthenticationTokenService private readonly authTokenService: IAuthenticationTokenService, @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, ) { super(); - this.userDataSyncStore = getUserDataSyncStore(configurationService); + this.userDataSyncStore = getUserDataSyncStore(productService, configurationService); } async read(key: string, oldValue: IUserData | null, source?: SyncSource): Promise { diff --git a/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts index 770865e5e26..6fa694e1d27 100644 --- a/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts @@ -8,6 +8,7 @@ import { Event } from 'vs/base/common/event'; import { IElectronService } from 'vs/platform/electron/node/electron'; import { UserDataAutoSyncService as BaseUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; export class UserDataAutoSyncService extends BaseUserDataAutoSyncService { @@ -17,15 +18,15 @@ export class UserDataAutoSyncService extends BaseUserDataAutoSyncService { @IElectronService electronService: IElectronService, @IUserDataSyncLogService logService: IUserDataSyncLogService, @IAuthenticationTokenService authTokenService: IAuthenticationTokenService, + @ITelemetryService telemetryService: ITelemetryService, ) { - super(userDataSyncEnablementService, userDataSyncService, logService, authTokenService); + super(userDataSyncEnablementService, userDataSyncService, logService, authTokenService, telemetryService); - // Sync immediately if there is a local change. - this._register(Event.debounce(Event.any( - electronService.onWindowFocus, - electronService.onWindowOpen, + this._register(Event.debounce(Event.any( + Event.map(electronService.onWindowFocus, () => 'windowFocus'), + Event.map(electronService.onWindowOpen, () => 'windowOpen'), userDataSyncService.onDidChangeLocal, - ), () => undefined, 500)(() => this.triggerAutoSync())); + ), (last, source) => last ? [...last, source] : [source], 1000)(sources => this.triggerAutoSync(sources))); } } diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index 396fc07139e..74a42082391 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -6,7 +6,7 @@ import { IRequestService } from 'vs/platform/request/common/request'; import { IRequestOptions, IRequestContext, IHeaders } from 'vs/base/parts/request/common/request'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IUserData, ResourceKey, IUserDataManifest, ALL_RESOURCE_KEYS, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSyncUtilService, IUserDataSyncEnablementService, ISettingsSyncService, IUserDataSyncService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserData, ResourceKey, IUserDataManifest, ALL_RESOURCE_KEYS, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSyncUtilService, IUserDataSyncEnablementService, ISettingsSyncService, IUserDataSyncService, getDefaultIgnoredSettings } from 'vs/platform/userDataSync/common/userDataSync'; import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; import { generateUuid } from 'vs/base/common/uuid'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; @@ -34,6 +34,8 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync'; import { Emitter } from 'vs/base/common/event'; import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication'; +import product from 'vs/platform/product/common/product'; +import { IProductService } from 'vs/platform/product/common/productService'; export class UserDataSyncClient extends Disposable { @@ -59,6 +61,8 @@ export class UserDataSyncClient extends Disposable { const logService = new NullLogService(); this.instantiationService.stub(ILogService, logService); + this.instantiationService.stub(IProductService, { _serviceBrand: undefined, ...product }); + const fileService = this._register(new FileService(logService)); fileService.registerProvider(Schemas.inMemory, new InMemoryFileSystemProvider()); this.instantiationService.stub(IFileService, fileService); @@ -231,6 +235,10 @@ export class TestUserDataSyncUtilService implements IUserDataSyncUtilService { _serviceBrand: any; + async resolveDefaultIgnoredSettings(): Promise { + return getDefaultIgnoredSettings(); + } + async resolveUserBindings(userbindings: string[]): Promise> { const keys: IStringDictionary = {}; for (const keybinding of userbindings) { diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 1d01f238f0d..52c6a90f94e 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -1806,7 +1806,7 @@ declare module 'vscode' { placeHolder?: string; /** - * Set to `true` to show a password prompt that will not show the typed value. + * Controls if a password input is shown. Password input hides the typed text. */ password?: boolean; @@ -4675,7 +4675,18 @@ declare module 'vscode' { * A code or identifier for this diagnostic. * Should be used for later processing, e.g. when providing [code actions](#CodeActionContext). */ - code?: string | number; + code?: string | number | { + /** + * A code or identifier for this diagnostic. + * Should be used for later processing, e.g. when providing [code actions](#CodeActionContext). + */ + value: string | number; + + /** + * A target URI to open with more information about the diagnostic error. + */ + target: Uri; + }; /** * An array of related diagnostic information, e.g. when symbol-names within diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index df4ac3e5f57..ff12027e949 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -20,7 +20,7 @@ declare module 'vscode' { export interface AuthenticationSession { id: string; - accessToken(): Promise; + accessToken(): Thenable; accountName: string; scopes: string[] } @@ -58,13 +58,13 @@ declare module 'vscode' { /** * Returns an array of current sessions. */ - getSessions(): Promise>; + getSessions(): Thenable>; /** * Prompts a user to login. */ - login(scopes: string[]): Promise; - logout(sessionId: string): Promise; + login(scopes: string[]): Thenable; + logout(sessionId: string): Thenable; } export namespace authentication { @@ -1187,65 +1187,76 @@ declare module 'vscode' { //#region Custom editors: https://github.com/microsoft/vscode/issues/77131 + // TODO: + // - Think about where a rename would live. + // - Think about handling go to line? (add other editor options? reveal?) + // - Should we expose edits? + // - More properties from `TextDocument`? + /** - * Defines the editing functionality of a webview editor. This allows the webview editor to hook into standard + * Defines the capabilities of a custom webview editor. + */ + interface CustomEditorCapabilities { + /** + * Defines the editing capability of a custom webview document. + * + * When not provided, the document is considered readonly. + */ + readonly editing?: CustomEditorEditingCapability; + } + + /** + * Defines the editing capability of a custom webview editor. This allows the webview editor to hook into standard * editor events such as `undo` or `save`. * * @param EditType Type of edits. */ - interface WebviewCustomEditorEditingDelegate { + interface CustomEditorEditingCapability { /** - * Save a resource. - * - * @param resource Resource being saved. + * Save the resource. * * @return Thenable signaling that the save has completed. */ - save(resource: Uri): Thenable; + save(): Thenable; /** - * Save an existing resource at a new path. + * Save the existing resource at a new path. * - * @param resource Resource being saved. * @param targetResource Location to save to. * * @return Thenable signaling that the save has completed. */ - saveAs(resource: Uri, targetResource: Uri): Thenable; + saveAs(targetResource: Uri): Thenable; /** * Event triggered by extensions to signal to VS Code that an edit has occurred. */ - // TODO@matt - // eslint-disable-next-line vscode-dts-event-naming - readonly onEdit: Event<{ readonly resource: Uri, readonly edit: EditType }>; + readonly onDidEdit: Event; /** * Apply a set of edits. * - * Note that is not invoked when `onEdit` is called as `onEdit` implies also updating the view to reflect the edit. + * Note that is not invoked when `onDidEdit` is called because `onDidEdit` implies also updating the view to reflect the edit. * - * @param resource Resource being edited. * @param edit Array of edits. Sorted from oldest to most recent. * * @return Thenable signaling that the change has completed. */ - applyEdits(resource: Uri, edits: readonly EditType[]): Thenable; + applyEdits(edits: readonly EditType[]): Thenable; /** * Undo a set of edits. * * This is triggered when a user undoes an edit or when revert is called on a file. * - * @param resource Resource being edited. * @param edit Array of edits. Sorted from most recent to oldest. * * @return Thenable signaling that the change has completed. */ - undoEdits(resource: Uri, edits: readonly EditType[]): Thenable; + undoEdits(edits: readonly EditType[]): Thenable; /** - * Back up `resource` in its current state. + * Back up the resource in its current state. * * Backups are used for hot exit and to prevent data loss. Your `backup` method should persist the resource in * its current state, i.e. with the edits applied. Most commonly this means saving the resource to disk in @@ -1257,56 +1268,119 @@ declare module 'vscode' { * made in quick succession, `backup` is only triggered after the last one. `backup` is not invoked when * `auto save` is enabled (since auto save already persists resource ). * - * @param resource The resource to back up. * @param cancellation Token that signals the current backup since a new backup is coming in. It is up to your * extension to decided how to respond to cancellation. If for example your extension is backing up a large file * in an operation that takes time to complete, your extension may decide to finish the ongoing backup rather * than cancelling it to ensure that VS Code has some valid backup. */ - backup?(resource: Uri, cancellation: CancellationToken): Thenable; + backup(cancellation: CancellationToken): Thenable; } - export interface WebviewCustomEditorProvider { + /** + * Represents a custom document for a custom webview editor. + * + * Custom documents are only used within a given `WebviewCustomEditorProvider`. The lifecycle of a + * `WebviewEditorCustomDocument` is managed by VS Code. When more more references remain to a given `WebviewEditorCustomDocument` + * then it is disposed of. + * + * @param UserDataType Type of custom object that extensions can store on the document. + */ + interface CustomDocument { + /** + * The associated viewType for this document. + */ + readonly viewType: string; + + /** + * The associated uri for this document. + */ + readonly uri: Uri; + + /** + * Event fired when there are no more references to the `WebviewEditorCustomDocument`. + */ + readonly onDidDispose: Event; + + /** + * Custom data that an extension can store on the document. + */ + userData?: UserDataType; + + // TODO: Should we expose edits here? + // This could be helpful for tracking the life cycle of edits + } + + /** + * Provider for webview editors that use a custom data model. + * + * Custom webview editors use [`WebviewEditorCustomDocument`](#WebviewEditorCustomDocument) as their data model. + * This gives extensions full control over actions such as edit, save, and backup. + * + * You should use custom text based editors when dealing with binary files or more complex scenarios. For simple text + * based documents, use [`WebviewTextEditorProvider`](#WebviewTextEditorProvider) instead. + */ + export interface CustomEditorProvider { + /** + * Create the model for a given + * + * @param document Resource being resolved. + */ + resolveCustomDocument(document: CustomDocument): Thenable; + /** * Resolve a webview editor for a given resource. * * To resolve a webview editor, a provider must fill in its initial html content and hook up all * the event listeners it is interested it. The provider should also take ownership of the passed in `WebviewPanel`. * - * @param resource Resource being resolved. - * @param webview Webview being resolved. The provider should take ownership of this webview. + * @param document Document for resource being resolved. + * @param webviewPanel Webview being resolved. The provider should take ownership of this webview. * * @return Thenable indicating that the webview editor has been resolved. */ - resolveWebviewEditor( - resource: Uri, - webview: WebviewPanel, - ): Thenable; + resolveCustomEditor(document: CustomDocument, webviewPanel: WebviewPanel): Thenable; + } + /** + * Provider for text based webview editors. + * + * Text based webview editors use a [`TextDocument`](#TextDocument) as their data model. This considerably simplifies + * implementing a webview editor as it allows VS Code to handle many common operations such as + * undo and backup. The provider is responsible for synchronizing text changes between the webview and the `TextDocument`. + * + * You should use text based webview editors when dealing with text based file formats, such as `xml` or `json`. + * For binary files or more specialized use cases, see [WebviewCustomEditorProvider](#WebviewCustomEditorProvider). + */ + export interface CustomTextEditorProvider { /** - * Controls the editing functionality of a webview editor. This allows the webview editor to hook into standard - * editor events such as `undo` or `save`. + * Resolve a webview editor for a given resource. * - * WebviewEditors that do not have `editingCapability` are considered to be readonly. Users can still interact - * with readonly editors, but these editors will not integrate with VS Code's standard editor functionality. + * To resolve a webview editor, the provider must fill in its initial html content and hook up all + * the event listeners it is interested it. The provider should also take ownership of the passed in `WebviewPanel`. + * + * @param document Resource being resolved. + * @param webviewPanel Webview being resolved. The provider should take ownership of this webview. + * + * @return Thenable indicating that the webview editor has been resolved. */ - readonly editingDelegate?: WebviewCustomEditorEditingDelegate; + resolveCustomTextEditor(document: TextDocument, webviewPanel: WebviewPanel): Thenable; } namespace window { /** - * Register a new provider for webview editors of a given type. + * Register a new provider for a custom editor. * - * @param viewType Type of the webview editor provider. - * @param provider Resolves webview editors. - * @param options Content settings for a webview panels the provider is given. + * @param viewType Type of the webview editor provider. This should match the `viewType` from the + * `package.json` contributions. + * @param provider Provider that resolves editors. + * @param webviewOptions Content settings for the webview panels that the provider is given. * - * @return Disposable that unregisters the `WebviewCustomEditorProvider`. + * @return Disposable that unregisters the provider. */ - export function registerWebviewCustomEditorProvider( + export function registerCustomEditorProvider( viewType: string, - provider: WebviewCustomEditorProvider, - options?: WebviewPanelOptions, + provider: CustomEditorProvider | CustomTextEditorProvider, + webviewOptions?: WebviewPanelOptions, ): Disposable; } @@ -1522,28 +1596,6 @@ declare module 'vscode' { //#endregion - //#region Diagnostic links https://github.com/microsoft/vscode/issues/11847 - - export interface Diagnostic { - /** - * Will be merged into `Diagnostic#code` - */ - code2?: { - /** - * A code or identifier for this diagnostic. - * Should be used for later processing, e.g. when providing [code actions](#CodeActionContext). - */ - value: string | number; - - /** - * A target URI to open with more information about the diagnostic error. - */ - target: Uri; - } - } - - //#endregion - //#region eamodio - timeline: https://github.com/microsoft/vscode/issues/84297 export class TimelineItem { @@ -1558,12 +1610,9 @@ declare module 'vscode' { label: string; /** - * Optional id for the timeline item. - */ - /** - * Optional id for the timeline item that has to be unique across your timeline source. + * Optional id for the timeline item. It must be unique across all the timeline items provided by this source. * - * If not provided, an id is generated using the timeline item's label. + * If not provided, an id is generated using the timeline item's timestamp. */ id?: string; @@ -1620,40 +1669,50 @@ declare module 'vscode' { * If the [uri](#Uri) is `undefined` that signals that the timeline source for all resources changed. */ uri?: Uri; - } - - export interface TimelineCursor { - /** - * A provider-defined cursor specifing the range of timeline items to be returned. Must be serializable. - */ - cursor?: any; /** - * A flag to specify whether the timeline items requested are before or after (default) the provided cursor. + * A flag which indicates whether the entire timeline should be reset. */ - before?: boolean; - - /** - * The maximum number of timeline items that should be returned. - */ - limit?: number; + reset?: boolean; } export interface Timeline { - /** - * A provider-defined cursor specifing the range of timeline items returned. Must be serializable. - */ - cursor?: any; + readonly paging?: { + /** + * A set of provider-defined cursors specifing the range of timeline items returned. + */ + readonly cursors: { + readonly before: string; + readonly after?: string + }; - /** - * A flag which indicates whether there are any more items that weren't returned. - */ - more?: boolean; + /** + * A flag which indicates whether there are more items that weren't returned. + */ + readonly more?: boolean; + } /** * An array of [timeline items](#TimelineItem). */ - items: TimelineItem[]; + readonly items: readonly TimelineItem[]; + } + + export interface TimelineOptions { + /** + * A provider-defined cursor specifing the range of timeline items that should be returned. + */ + cursor?: string; + + /** + * A flag to specify whether the timeline items being requested should be before or after (default) the provided cursor. + */ + before?: boolean; + + /** + * The maximum number or the ending cursor of timeline items that should be returned. + */ + limit?: number | string; } export interface TimelineProvider { @@ -1666,23 +1725,23 @@ declare module 'vscode' { /** * An identifier of the source of the timeline items. This can be used to filter sources. */ - id: string; + readonly id: string; /** * A human-readable string describing the source of the timeline items. This can be used as the display label when filtering sources. */ - label: string; + readonly label: string; /** * Provide [timeline items](#TimelineItem) for a [Uri](#Uri). * * @param uri The [uri](#Uri) of the file to provide the timeline for. + * @param options A set of options to determine how results should be returned. * @param token A cancellation token. - * @param cursor TBD * @return The [timeline result](#TimelineResult) or a thenable that resolves to such. The lack of a result * can be signaled by returning `undefined`, `null`, or an empty array. */ - provideTimeline(uri: Uri, cursor: TimelineCursor, token: CancellationToken): ProviderResult; + provideTimeline(uri: Uri, options: TimelineOptions, token: CancellationToken): ProviderResult; } export namespace workspace { diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index 311d89790db..22f91f6d2cc 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -75,48 +75,52 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu async $getSessionsPrompt(providerId: string, providerName: string, extensionId: string, extensionName: string): Promise { const alwaysAllow = this.storageService.get(`${extensionId}-${providerId}`, StorageScope.GLOBAL); if (alwaysAllow) { - return true; + return alwaysAllow === 'true'; } - const { choice } = await this.dialogService.show( + const { choice, checkboxChecked } = await this.dialogService.show( Severity.Info, nls.localize('confirmAuthenticationAccess', "The extension '{0}' is trying to access authentication information from {1}.", extensionName, providerName), - [nls.localize('cancel', "Cancel"), nls.localize('allow', "Allow"), nls.localize('alwaysAllow', "Always Allow"),], - { cancelId: 0 } + [nls.localize('cancel', "Cancel"), nls.localize('allow', "Allow")], + { + cancelId: 0, + checkbox: { + label: nls.localize('neverAgain', "Don't Show Again") + } + } ); - switch (choice) { - case 1/** Allow */: - return true; - case 2 /** Always Allow */: - this.storageService.store(`${extensionId}-${providerId}`, 'true', StorageScope.GLOBAL); - return true; - default: - return false; + const allow = choice === 1; + if (checkboxChecked) { + this.storageService.store(`${extensionId}-${providerId}`, allow ? 'true' : 'false', StorageScope.GLOBAL); } + + return allow; } async $loginPrompt(providerId: string, providerName: string, extensionId: string, extensionName: string): Promise { const alwaysAllow = this.storageService.get(`${extensionId}-${providerId}`, StorageScope.GLOBAL); if (alwaysAllow) { - return true; + return alwaysAllow === 'true'; } - const { choice } = await this.dialogService.show( + const { choice, checkboxChecked } = await this.dialogService.show( Severity.Info, nls.localize('confirmLogin', "The extension '{0}' wants to sign in using {1}.", extensionName, providerName), - [nls.localize('cancel', "Cancel"), nls.localize('continue', "Continue"), nls.localize('neverAgain', "Don't Show Again")], - { cancelId: 0 } + [nls.localize('cancel', "Cancel"), nls.localize('continue', "Continue")], + { + cancelId: 0, + checkbox: { + label: nls.localize('neverAgain', "Don't Show Again") + } + } ); - switch (choice) { - case 1/** Allow */: - return true; - case 2 /** Always Allow */: - this.storageService.store(`${extensionId}-${providerId}`, 'true', StorageScope.GLOBAL); - return true; - default: - return false; + const allow = choice === 1; + if (checkboxChecked) { + this.storageService.store(`${extensionId}-${providerId}`, allow ? 'true' : 'false', StorageScope.GLOBAL); } + + return allow; } } diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 8851128af0e..4287e53a8c0 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -273,7 +273,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha // --- quick fix - $registerQuickFixSupport(handle: number, selector: IDocumentFilterDto[], metadata: ICodeActionProviderMetadataDto): void { + $registerQuickFixSupport(handle: number, selector: IDocumentFilterDto[], metadata: ICodeActionProviderMetadataDto, displayName: string): void { this._registrations.set(handle, modes.CodeActionProviderRegistry.register(selector, { provideCodeActions: async (model: ITextModel, rangeOrSelection: EditorRange | Selection, context: modes.CodeActionContext, token: CancellationToken): Promise => { const listDto = await this._proxy.$provideCodeActions(handle, model.uri, rangeOrSelection, context, token); @@ -290,7 +290,8 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha }; }, providedCodeActionKinds: metadata.providedKinds, - documentation: metadata.documentation + documentation: metadata.documentation, + displayName })); } diff --git a/src/vs/workbench/api/browser/mainThreadTask.ts b/src/vs/workbench/api/browser/mainThreadTask.ts index f78dc6ca963..ede1ae5c393 100644 --- a/src/vs/workbench/api/browser/mainThreadTask.ts +++ b/src/vs/workbench/api/browser/mainThreadTask.ts @@ -512,7 +512,7 @@ export class MainThreadTask implements MainThreadTaskShape { public $executeTask(value: TaskHandleDTO | TaskDTO): Promise { return new Promise((resolve, reject) => { if (TaskHandleDTO.is(value)) { - const workspaceFolder = this._workspaceContextServer.getWorkspaceFolder(URI.revive(value.workspaceFolder)); + const workspaceFolder = typeof value.workspaceFolder === 'string' ? value.workspaceFolder : this._workspaceContextServer.getWorkspaceFolder(URI.revive(value.workspaceFolder)); if (workspaceFolder) { this._taskService.getTask(workspaceFolder, value.id, true).then((task: Task | undefined) => { if (!task) { diff --git a/src/vs/workbench/api/browser/mainThreadTimeline.ts b/src/vs/workbench/api/browser/mainThreadTimeline.ts index 428bf0ed2d9..cfeb1c6c38c 100644 --- a/src/vs/workbench/api/browser/mainThreadTimeline.ts +++ b/src/vs/workbench/api/browser/mainThreadTimeline.ts @@ -9,7 +9,7 @@ import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { MainContext, MainThreadTimelineShape, IExtHostContext, ExtHostTimelineShape, ExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; -import { TimelineChangeEvent, TimelineCursor, TimelineProviderDescriptor, ITimelineService } from 'vs/workbench/contrib/timeline/common/timeline'; +import { TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, ITimelineService } from 'vs/workbench/contrib/timeline/common/timeline'; @extHostNamedCustomer(MainContext.MainThreadTimeline) export class MainThreadTimeline implements MainThreadTimelineShape { @@ -39,8 +39,8 @@ export class MainThreadTimeline implements MainThreadTimelineShape { this._timelineService.registerTimelineProvider({ ...provider, onDidChange: onDidChange.event, - provideTimeline(uri: URI, cursor: TimelineCursor, token: CancellationToken, options?: { cacheResults?: boolean }) { - return proxy.$getTimeline(provider.id, uri, cursor, token, options); + provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: { cacheResults?: boolean }) { + return proxy.$getTimeline(provider.id, uri, options, token, internalOptions); }, dispose() { emitters.delete(provider.id); diff --git a/src/vs/workbench/api/browser/mainThreadWebview.ts b/src/vs/workbench/api/browser/mainThreadWebview.ts index 6a006f007f1..dda6ad071d9 100644 --- a/src/vs/workbench/api/browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/browser/mainThreadWebview.ts @@ -21,7 +21,7 @@ import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; import { editorGroupToViewColumn, EditorViewColumn, viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor'; import { IEditorInput } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; -import { CustomFileEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput'; +import { CustomEditorInput, ModelType } from 'vs/workbench/contrib/customEditor/browser/customEditorInput'; import { ICustomEditorModel, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { WebviewExtensionDescription } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput'; @@ -97,7 +97,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma private readonly _webviewInputs = new WebviewInputStore(); private readonly _revivers = new Map(); private readonly _editorProviders = new Map(); - private readonly _customEditorModels = new Map(); + private readonly _customEditorModels = new Map(); constructor( context: extHostProtocol.IExtHostContext, @@ -121,8 +121,8 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma // This should trigger the real reviver to be registered from the extension host side. this._register(_webviewWorkbenchService.registerResolver({ canResolve: (webview: WebviewInput) => { - if (webview instanceof CustomFileEditorInput) { - extensionService.activateByEvent(`onWebviewEditor:${webview.viewType}`); + if (webview instanceof CustomEditorInput) { + extensionService.activateByEvent(`onCustomEditor:${webview.viewType}`); return false; } @@ -256,7 +256,20 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma this._revivers.delete(viewType); } - public $registerEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, capabilities: readonly extHostProtocol.WebviewEditorCapabilities[]): void { + public $registerTextEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions): void { + return this.registerEditorProvider(ModelType.Text, extensionData, viewType, options); + } + + public $registerCustomEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions): void { + return this.registerEditorProvider(ModelType.Custom, extensionData, viewType, options); + } + + public registerEditorProvider( + modelType: ModelType, + extensionData: extHostProtocol.WebviewExtensionDescription, + viewType: string, + options: modes.IWebviewPanelOptions, + ): void { if (this._editorProviders.has(viewType)) { throw new Error(`Provider for ${viewType} already registered`); } @@ -265,21 +278,25 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma this._editorProviders.set(viewType, this._webviewWorkbenchService.registerResolver({ canResolve: (webviewInput) => { - return webviewInput instanceof CustomFileEditorInput && webviewInput.viewType === viewType; + return webviewInput instanceof CustomEditorInput && webviewInput.viewType === viewType; }, - resolveWebview: async (webviewInput: CustomFileEditorInput) => { + resolveWebview: async (webviewInput: CustomEditorInput) => { const handle = webviewInput.id; this._webviewInputs.add(handle, webviewInput); this.hookupWebviewEventDelegate(handle, webviewInput); webviewInput.webview.options = options; webviewInput.webview.extension = extension; + webviewInput.modelType = modelType; + const resource = webviewInput.resource; - const model = await this.retainCustomEditorModel(webviewInput, resource, viewType, capabilities); - webviewInput.onDisposeWebview(() => { - this.releaseCustomEditorModel(model); - }); + if (modelType === ModelType.Custom) { + const model = await this.retainCustomEditorModel(webviewInput, resource, viewType); + webviewInput.onDisposeWebview(() => { + this.releaseCustomEditorModel(model); + }); + } try { await this._proxy.$resolveWebviewEditor( @@ -311,33 +328,26 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma this._customEditorService.models.disposeAllModelsForView(viewType); } - private async retainCustomEditorModel(webviewInput: WebviewInput, resource: URI, viewType: string, capabilities: readonly extHostProtocol.WebviewEditorCapabilities[]) { + private async retainCustomEditorModel(webviewInput: WebviewInput, resource: URI, viewType: string) { const model = await this._customEditorService.models.resolve(webviewInput.resource, webviewInput.viewType); - const existingEntry = this._customEditorModels.get(model); + const key = viewType + resource.toString(); + const existingEntry = this._customEditorModels.get(key); if (existingEntry) { ++existingEntry.referenceCount; // no need to hook up listeners again return model; } + this._customEditorModels.set(key, { referenceCount: 1 }); + const { editable } = await this._proxy.$createWebviewCustomEditorDocument(resource, viewType); - this._customEditorModels.set(model, { referenceCount: 1 }); - - const capabilitiesSet = new Set(capabilities); - const isEditable = capabilitiesSet.has(extHostProtocol.WebviewEditorCapabilities.Editable); - if (isEditable) { - model.onUndo(e => { - this._proxy.$undoEdits(resource, viewType, e.edits); + if (editable) { + model.onUndo(() => { + this._proxy.$undo(resource, viewType); }); - model.onDisposeEdits(e => { - this._proxy.$disposeEdits(e.edits); - }); - - model.onApplyEdit(e => { - if (e.trigger !== model) { - this._proxy.$applyEdits(resource, viewType, e.edits); - } + model.onRedo(() => { + this._proxy.$redo(resource, viewType); }); model.onWillSave(e => { @@ -347,7 +357,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma // Save as should always be implemented even if the model is readonly model.onWillSaveAs(e => { - if (isEditable) { + if (editable) { e.waitUntil(this._proxy.$onSaveAs(e.resource.toJSON(), viewType, e.targetResource.toJSON())); } else { // Since the editor is readonly, just copy the file over @@ -355,36 +365,37 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma } }); - if (capabilitiesSet.has(extHostProtocol.WebviewEditorCapabilities.SupportsHotExit)) { - model.onBackup(() => { - return createCancelablePromise(token => - this._proxy.$backup(model.resource.toJSON(), viewType, token)); - }); - } + model.onBackup(() => { + return createCancelablePromise(token => + this._proxy.$backup(model.resource.toJSON(), viewType, token)); + }); return model; } private async releaseCustomEditorModel(model: ICustomEditorModel) { - const entry = this._customEditorModels.get(model); + const key = model.viewType + model.resource; + const entry = this._customEditorModels.get(key); if (!entry) { - return; + throw new Error('Model not found'); } --entry.referenceCount; if (entry.referenceCount <= 0) { + this._proxy.$disposeWebviewCustomEditorDocument(model.resource, model.viewType); this._customEditorService.models.disposeModel(model); - this._customEditorModels.delete(model); + this._customEditorModels.delete(key); } } - public $onEdit(resource: UriComponents, viewType: string, editId: number): void { + + + public $onDidChangeCustomDocumentState(resource: UriComponents, viewType: string, state: { dirty: boolean }) { const model = this._customEditorService.models.get(URI.revive(resource), viewType); if (!model) { throw new Error('Could not find model for webview editor'); } - - model.pushEdit(editId, model); + model.setDirty(state.dirty); } private hookupWebviewEventDelegate(handle: extHostProtocol.WebviewPanelHandle, input: WebviewInput) { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index dfd2ec920ea..8929e1c1aed 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -114,7 +114,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostOutputService = rpcProtocol.set(ExtHostContext.ExtHostOutputService, accessor.get(IExtHostOutputService)); // manually create and register addressable instances - const extHostWebviews = rpcProtocol.set(ExtHostContext.ExtHostWebviews, new ExtHostWebviews(rpcProtocol, initData.environment, extHostWorkspace, extHostLogService, extHostApiDeprecation)); const extHostUrls = rpcProtocol.set(ExtHostContext.ExtHostUrls, new ExtHostUrls(rpcProtocol)); const extHostDocuments = rpcProtocol.set(ExtHostContext.ExtHostDocuments, new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors)); const extHostDocumentContentProviders = rpcProtocol.set(ExtHostContext.ExtHostDocumentContentProviders, new ExtHostDocumentContentProvider(rpcProtocol, extHostDocumentsAndEditors, extHostLogService)); @@ -136,6 +135,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostTheming = rpcProtocol.set(ExtHostContext.ExtHostTheming, new ExtHostTheming(rpcProtocol)); const extHostAuthentication = rpcProtocol.set(ExtHostContext.ExtHostAuthentication, new ExtHostAuthentication(rpcProtocol)); const extHostTimeline = rpcProtocol.set(ExtHostContext.ExtHostTimeline, new ExtHostTimeline(rpcProtocol, extHostCommands)); + const extHostWebviews = rpcProtocol.set(ExtHostContext.ExtHostWebviews, new ExtHostWebviews(rpcProtocol, initData.environment, extHostWorkspace, extHostLogService, extHostApiDeprecation, extHostDocuments)); // Check that no named customers are missing const expected: ProxyIdentifier[] = values(ExtHostContext); @@ -563,9 +563,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerWebviewPanelSerializer: (viewType: string, serializer: vscode.WebviewPanelSerializer) => { return extHostWebviews.registerWebviewPanelSerializer(extension, viewType, serializer); }, - registerWebviewCustomEditorProvider: (viewType: string, provider: vscode.WebviewCustomEditorProvider, options?: vscode.WebviewPanelOptions) => { + registerCustomEditorProvider: (viewType: string, provider: vscode.CustomEditorProvider | vscode.CustomTextEditorProvider, options?: vscode.WebviewPanelOptions) => { checkProposedApiEnabled(extension); - return extHostWebviews.registerWebviewCustomEditorProvider(extension, viewType, provider, options); + return extHostWebviews.registerCustomEditorProvider(extension, viewType, provider, options); }, registerDecorationProvider(provider: vscode.DecorationProvider) { checkProposedApiEnabled(extension); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 326badb63c2..e91e3341e57 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -49,7 +49,7 @@ import { SaveReason } from 'vs/workbench/common/editor'; import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator'; import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; import { TunnelOptions } from 'vs/platform/remote/common/tunnel'; -import { Timeline, TimelineChangeEvent, TimelineCursor, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline'; +import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline'; import { revive } from 'vs/base/common/marshalling'; import { INotebookMimeTypeSelector, IOutput, INotebookDisplayOrder } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; @@ -362,7 +362,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerEvaluatableExpressionProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerDocumentHighlightProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerReferenceSupport(handle: number, selector: IDocumentFilterDto[]): void; - $registerQuickFixSupport(handle: number, selector: IDocumentFilterDto[], metadata: ICodeActionProviderMetadataDto): void; + $registerQuickFixSupport(handle: number, selector: IDocumentFilterDto[], metadata: ICodeActionProviderMetadataDto, displayName: string): void; $registerDocumentFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void; $registerRangeFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void; $registerOnTypeFormattingSupport(handle: number, selector: IDocumentFilterDto[], autoFormatTriggerCharacters: string[], extensionId: ExtensionIdentifier): void; @@ -598,10 +598,11 @@ export interface MainThreadWebviewsShape extends IDisposable { $registerSerializer(viewType: string): void; $unregisterSerializer(viewType: string): void; - $registerEditorProvider(extension: WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, capabilities: readonly WebviewEditorCapabilities[]): void; + $registerTextEditorProvider(extension: WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions): void; + $registerCustomEditorProvider(extension: WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions): void; $unregisterEditorProvider(viewType: string): void; - $onEdit(resource: UriComponents, viewType: string, editId: number): void; + $onDidChangeCustomDocumentState(resource: UriComponents, viewType: string, state: { dirty: boolean }): void; } export interface WebviewPanelViewStateData { @@ -619,12 +620,14 @@ export interface ExtHostWebviewsShape { $onDidDisposeWebviewPanel(handle: WebviewPanelHandle): Promise; $deserializeWebviewPanel(newWebviewHandle: WebviewPanelHandle, viewType: string, title: string, state: any, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions): Promise; + $resolveWebviewEditor(resource: UriComponents, newWebviewHandle: WebviewPanelHandle, viewType: string, title: string, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions): Promise; + $createWebviewCustomEditorDocument(resource: UriComponents, viewType: string): Promise<{ editable: boolean }>; + $disposeWebviewCustomEditorDocument(resource: UriComponents, viewType: string): Promise; - $undoEdits(resource: UriComponents, viewType: string, editIds: readonly number[]): void; - $applyEdits(resource: UriComponents, viewType: string, editIds: readonly number[]): void; - $disposeEdits(editIds: readonly number[]): void; - + $undo(resource: UriComponents, viewType: string): void; + $redo(resource: UriComponents, viewType: string): void; + $revert(resource: UriComponents, viewType: string): void; $onSave(resource: UriComponents, viewType: string): Promise; $onSaveAs(resource: UriComponents, viewType: string, targetResource: UriComponents): Promise; @@ -1518,7 +1521,7 @@ export interface ExtHostTunnelServiceShape { } export interface ExtHostTimelineShape { - $getTimeline(source: string, uri: UriComponents, cursor: TimelineCursor, token: CancellationToken, options?: { cacheResults?: boolean }): Promise; + $getTimeline(source: string, uri: UriComponents, options: TimelineOptions, token: CancellationToken, internalOptions?: { cacheResults?: boolean }): Promise; } // --- proxy identifiers diff --git a/src/vs/workbench/api/common/extHostAuthentication.ts b/src/vs/workbench/api/common/extHostAuthentication.ts index d9bcc811cbf..234de7348f3 100644 --- a/src/vs/workbench/api/common/extHostAuthentication.ts +++ b/src/vs/workbench/api/common/extHostAuthentication.ts @@ -60,7 +60,7 @@ export class AuthenticationProviderWrapper implements vscode.AuthenticationProvi return this._provider.login(scopes); } - logout(sessionId: string): Promise { + logout(sessionId: string): Thenable { return this._provider.logout(sessionId); } } diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index c0522477836..479a5126b3c 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -1617,7 +1617,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF kind: x.kind.value, command: this._commands.converter.toInternal(x.command, store), })) - }); + }, ExtHostLanguageFeatures._extLabel(extension)); store.add(this._createDisposable(handle)); return store; } diff --git a/src/vs/workbench/api/common/extHostTask.ts b/src/vs/workbench/api/common/extHostTask.ts index 8ae923128bc..71430153a3d 100644 --- a/src/vs/workbench/api/common/extHostTask.ts +++ b/src/vs/workbench/api/common/extHostTask.ts @@ -26,6 +26,7 @@ import { Schemas } from 'vs/base/common/network'; import * as Platform from 'vs/base/common/platform'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; +import { USER_TASKS_GROUP_KEY } from 'vs/workbench/contrib/tasks/common/taskService'; export interface IExtHostTask extends ExtHostTaskShape { @@ -192,9 +193,11 @@ export namespace CustomExecutionDTO { export namespace TaskHandleDTO { export function from(value: types.Task): tasks.TaskHandleDTO { - let folder: UriComponents | undefined; + let folder: UriComponents | string; if (value.scope !== undefined && typeof value.scope !== 'number') { folder = value.scope.uri; + } else if (value.scope !== undefined && typeof value.scope === 'number') { + folder = USER_TASKS_GROUP_KEY; } return { id: value._id!, diff --git a/src/vs/workbench/api/common/extHostTimeline.ts b/src/vs/workbench/api/common/extHostTimeline.ts index f5f63a2ce4d..87fefc82cfc 100644 --- a/src/vs/workbench/api/common/extHostTimeline.ts +++ b/src/vs/workbench/api/common/extHostTimeline.ts @@ -7,7 +7,7 @@ import * as vscode from 'vscode'; import { UriComponents, URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ExtHostTimelineShape, MainThreadTimelineShape, IMainContext, MainContext } from 'vs/workbench/api/common/extHost.protocol'; -import { Timeline, TimelineCursor, TimelineItem, TimelineProvider } from 'vs/workbench/contrib/timeline/common/timeline'; +import { Timeline, TimelineItem, TimelineOptions, TimelineProvider } from 'vs/workbench/contrib/timeline/common/timeline'; import { IDisposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { CancellationToken } from 'vs/base/common/cancellation'; import { CommandsConverter, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; @@ -16,7 +16,7 @@ import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; export interface IExtHostTimeline extends ExtHostTimelineShape { readonly _serviceBrand: undefined; - $getTimeline(id: string, uri: UriComponents, cursor: vscode.TimelineCursor, token: vscode.CancellationToken, options?: { cacheResults?: boolean }): Promise; + $getTimeline(id: string, uri: UriComponents, options: vscode.TimelineOptions, token: vscode.CancellationToken, internalOptions?: { cacheResults?: boolean }): Promise; } export const IExtHostTimeline = createDecorator('IExtHostTimeline'); @@ -50,9 +50,9 @@ export class ExtHostTimeline implements IExtHostTimeline { }); } - async $getTimeline(id: string, uri: UriComponents, cursor: vscode.TimelineCursor, token: vscode.CancellationToken, options?: { cacheResults?: boolean }): Promise { + async $getTimeline(id: string, uri: UriComponents, options: vscode.TimelineOptions, token: vscode.CancellationToken, internalOptions?: { cacheResults?: boolean }): Promise { const provider = this._providers.get(id); - return provider?.provideTimeline(URI.revive(uri), cursor, token, options); + return provider?.provideTimeline(URI.revive(uri), options, token, internalOptions); } registerTimelineProvider(scheme: string | string[], provider: vscode.TimelineProvider, _extensionId: ExtensionIdentifier, commandConverter: CommandsConverter): IDisposable { @@ -70,15 +70,15 @@ export class ExtHostTimeline implements IExtHostTimeline { ...provider, scheme: scheme, onDidChange: undefined, - async provideTimeline(uri: URI, cursor: TimelineCursor, token: CancellationToken, options?: { cacheResults?: boolean }) { + async provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: { cacheResults?: boolean }) { timelineDisposables.clear(); // For now, only allow the caching of a single Uri - if (options?.cacheResults && !itemsBySourceByUriMap.has(getUriKey(uri))) { + if (internalOptions?.cacheResults && !itemsBySourceByUriMap.has(getUriKey(uri))) { itemsBySourceByUriMap.clear(); } - const result = await provider.provideTimeline(uri, cursor, token); + const result = await provider.provideTimeline(uri, options, token); // Intentional == we don't know how a provider will respond // eslint-disable-next-line eqeqeq if (result == null) { @@ -86,7 +86,7 @@ export class ExtHostTimeline implements IExtHostTimeline { } // TODO: Determine if we should cache dependent on who calls us (internal vs external) - const convertItem = convertTimelineItem(uri, options?.cacheResults ?? false); + const convertItem = convertTimelineItem(uri, internalOptions?.cacheResults ?? false); return { ...result, source: provider.id, @@ -143,6 +143,7 @@ export class ExtHostTimeline implements IExtHostTimeline { return { ...props, + id: props.id ?? undefined, handle: handle, source: source, command: item.command ? commandConverter.toInternal(item.command, disposables) : undefined, diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 6027377e166..38154ceca2e 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -127,12 +127,17 @@ export namespace DiagnosticTag { export namespace Diagnostic { export function from(value: vscode.Diagnostic): IMarkerData { - let code: string | { value: string; target: URI } | undefined = isString(value.code) || isNumber(value.code) ? String(value.code) : undefined; - if (value.code2) { - code = { - value: String(value.code2.value), - target: value.code2.target - }; + let code: string | { value: string; target: URI } | undefined; + + if (value.code) { + if (isString(value.code) || isNumber(value.code)) { + code = String(value.code); + } else { + code = { + value: String(value.code.value), + target: value.code.target, + }; + } } return { diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index 4eed027e83e..539f31df1c2 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; @@ -10,16 +11,15 @@ import { generateUuid } from 'vs/base/common/uuid'; import * as modes from 'vs/editor/common/modes'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; +import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; +import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; import type * as vscode from 'vscode'; -import { Cache } from './cache'; -import { ExtHostWebviewsShape, IMainContext, MainContext, MainThreadWebviewsShape, WebviewEditorCapabilities, WebviewPanelHandle, WebviewPanelViewStateData } from './extHost.protocol'; +import { ExtHostWebviewsShape, IMainContext, MainContext, MainThreadWebviewsShape, WebviewExtensionDescription, WebviewPanelHandle, WebviewPanelViewStateData } from './extHost.protocol'; import { Disposable as VSCodeDisposable } from './extHostTypes'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; type IconPath = URI | { light: URI, dark: URI }; @@ -245,6 +245,218 @@ export class ExtHostWebviewEditor extends Disposable implements vscode.WebviewPa } } +type EditType = unknown; + +class CustomDocument extends Disposable implements vscode.CustomDocument { + + public static create(proxy: MainThreadWebviewsShape, viewType: string, uri: vscode.Uri) { + return Object.seal(new CustomDocument(proxy, viewType, uri)); + } + + // Explicitly initialize all properties as we seal the object after creation! + + #currentEditIndex: number = -1; + #savePoint: number = -1; + readonly #edits: Array = []; + + readonly #proxy: MainThreadWebviewsShape; + readonly #viewType: string; + readonly #uri: vscode.Uri; + + #capabilities: vscode.CustomEditorCapabilities | undefined = undefined; + + private constructor(proxy: MainThreadWebviewsShape, viewType: string, uri: vscode.Uri) { + super(); + this.#proxy = proxy; + this.#viewType = viewType; + this.#uri = uri; + } + + dispose() { + this.#onDidDispose.fire(); + super.dispose(); + } + + //#region Public API + + public get viewType(): string { return this.#viewType; } + + public get uri(): vscode.Uri { return this.#uri; } + + #onDidDispose = this._register(new Emitter()); + public readonly onDidDispose = this.#onDidDispose.event; + + public userData: unknown = undefined; + + //#endregion + + //#region Internal + + /** @internal*/ _setCapabilities(capabilities: vscode.CustomEditorCapabilities) { + if (this.#capabilities) { + throw new Error('Capabilities already provided'); + } + + this.#capabilities = capabilities; + capabilities.editing?.onDidEdit(edit => { + this.pushEdit(edit); + }); + } + + /** @internal*/ _revert() { + const editing = this.getEditingCapability(); + if (this.#currentEditIndex === this.#savePoint) { + return true; + } + + if (this.#currentEditIndex >= this.#savePoint) { + const editsToUndo = this.#edits.slice(this.#savePoint, this.#currentEditIndex); + editing.undoEdits(editsToUndo.reverse()); + } else if (this.#currentEditIndex < this.#savePoint) { + const editsToRedo = this.#edits.slice(this.#currentEditIndex, this.#savePoint); + editing.applyEdits(editsToRedo); + } + + this.#currentEditIndex = this.#savePoint; + this.spliceEdits(); + + this.updateState(); + return true; + } + + /** @internal*/ _undo() { + const editing = this.getEditingCapability(); + if (this.#currentEditIndex < 0) { + // nothing to undo + return; + } + + const undoneEdit = this.#edits[this.#currentEditIndex]; + --this.#currentEditIndex; + editing.undoEdits([undoneEdit]); + this.updateState(); + } + + /** @internal*/ _redo() { + const editing = this.getEditingCapability(); + if (this.#currentEditIndex >= this.#edits.length - 1) { + // nothing to redo + return; + } + + ++this.#currentEditIndex; + const redoneEdit = this.#edits[this.#currentEditIndex]; + editing.applyEdits([redoneEdit]); + this.updateState(); + } + + /** @internal*/ _save() { + return this.getEditingCapability().save(); + } + + /** @internal*/ _saveAs(target: vscode.Uri) { + return this.getEditingCapability().saveAs(target); + } + + /** @internal*/ _backup(cancellation: CancellationToken) { + return this.getEditingCapability().backup(cancellation); + } + + //#endregion + + private pushEdit(edit: EditType) { + this.spliceEdits(edit); + + this.#currentEditIndex = this.#edits.length - 1; + this.updateState(); + } + + private updateState() { + const dirty = this.#edits.length > 0 && this.#savePoint !== this.#currentEditIndex; + this.#proxy.$onDidChangeCustomDocumentState(this.uri, this.viewType, { dirty }); + } + + private spliceEdits(editToInsert?: EditType) { + const start = this.#currentEditIndex + 1; + const toRemove = this.#edits.length - this.#currentEditIndex; + + editToInsert + ? this.#edits.splice(start, toRemove, editToInsert) + : this.#edits.splice(start, toRemove); + } + + private getEditingCapability(): vscode.CustomEditorEditingCapability { + if (!this.#capabilities?.editing) { + throw new Error('Document is not editable'); + } + return this.#capabilities.editing; + } +} + +class WebviewDocumentStore { + private readonly _documents = new Map(); + + public get(viewType: string, resource: vscode.Uri): CustomDocument | undefined { + return this._documents.get(this.key(viewType, resource)); + } + + public add(document: CustomDocument) { + const key = this.key(document.viewType, document.uri); + if (this._documents.has(key)) { + throw new Error(`Document already exists for viewType:${document.viewType} resource:${document.uri}`); + } + this._documents.set(key, document); + } + + public delete(document: CustomDocument) { + const key = this.key(document.viewType, document.uri); + this._documents.delete(key); + } + + private key(viewType: string, resource: vscode.Uri): string { + return `${viewType}@@@${resource}`; + } +} + +const enum WebviewEditorType { + Text, + Custom +} + +type ProviderEntry = { + readonly extension: IExtensionDescription; + readonly type: WebviewEditorType.Text; + readonly provider: vscode.CustomTextEditorProvider; +} | { + readonly extension: IExtensionDescription; + readonly type: WebviewEditorType.Custom; + readonly provider: vscode.CustomEditorProvider; +}; + +class EditorProviderStore { + private readonly _providers = new Map(); + + public addTextProvider(viewType: string, extension: IExtensionDescription, provider: vscode.CustomTextEditorProvider): vscode.Disposable { + return this.add(WebviewEditorType.Text, viewType, extension, provider); + } + + public addCustomProvider(viewType: string, extension: IExtensionDescription, provider: vscode.CustomEditorProvider): vscode.Disposable { + return this.add(WebviewEditorType.Custom, viewType, extension, provider); + } + + public get(viewType: string): ProviderEntry | undefined { + return this._providers.get(viewType); + } + + private add(type: WebviewEditorType, viewType: string, extension: IExtensionDescription, provider: vscode.CustomTextEditorProvider | vscode.CustomEditorProvider): vscode.Disposable { + if (this._providers.has(viewType)) { + throw new Error(`Provider for viewType:${viewType} already registered`); + } + this._providers.set(viewType, { type, extension, provider } as ProviderEntry); + return new VSCodeDisposable(() => this._providers.delete(viewType)); + } +} + export class ExtHostWebviews implements ExtHostWebviewsShape { private static newHandle(): WebviewPanelHandle { @@ -259,12 +471,9 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { readonly extension: IExtensionDescription; }>(); - private readonly _editorProviders = new Map(); + private readonly _editorProviders = new EditorProviderStore(); - private readonly _edits = new Cache('edits'); + private readonly _documents = new WebviewDocumentStore(); constructor( mainContext: IMainContext, @@ -272,6 +481,7 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { private readonly workspace: IExtHostWorkspace | undefined, private readonly _logService: ILogService, private readonly _deprecationService: IExtHostApiDeprecationService, + private readonly _extHostDocuments: ExtHostDocuments, ) { this._proxy = mainContext.getProxy(MainContext.MainThreadWebviews); } @@ -290,7 +500,7 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { }; const handle = ExtHostWebviews.newHandle(); - this._proxy.$createWebviewPanel({ id: extension.identifier, location: extension.extensionLocation }, handle, viewType, title, webviewShowOptions, convertWebviewOptions(extension, this.workspace, options)); + this._proxy.$createWebviewPanel(toExtensionData(extension), handle, viewType, title, webviewShowOptions, convertWebviewOptions(extension, this.workspace, options)); const webview = new ExtHostWebview(handle, this._proxy, options, this.initData, this.workspace, extension, this._deprecationService); const panel = new ExtHostWebviewEditor(handle, this._proxy, viewType, title, viewColumn, options, webview); @@ -316,29 +526,26 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { }); } - public registerWebviewCustomEditorProvider( + public registerCustomEditorProvider( extension: IExtensionDescription, viewType: string, - provider: vscode.WebviewCustomEditorProvider, - options?: vscode.WebviewPanelOptions, + provider: vscode.CustomEditorProvider | vscode.CustomTextEditorProvider, + options: vscode.WebviewPanelOptions | undefined = {} ): vscode.Disposable { - if (this._editorProviders.has(viewType)) { - throw new Error(`Editor provider for '${viewType}' already registered`); + let disposable: vscode.Disposable; + if ('resolveCustomTextEditor' in provider) { + disposable = this._editorProviders.addTextProvider(viewType, extension, provider); + this._proxy.$registerTextEditorProvider(toExtensionData(extension), viewType, options); + } else { + disposable = this._editorProviders.addCustomProvider(viewType, extension, provider); + this._proxy.$registerCustomEditorProvider(toExtensionData(extension), viewType, options); } - this._editorProviders.set(viewType, { extension, provider, }); - this._proxy.$registerEditorProvider({ id: extension.identifier, location: extension.extensionLocation }, viewType, options || {}, this.getCapabilites(provider)); - - // Hook up events - provider?.editingDelegate?.onEdit(({ edit, resource }) => { - const id = this._edits.add([edit]); - this._proxy.$onEdit(resource, viewType, id); - }); - - return new VSCodeDisposable(() => { - this._editorProviders.delete(viewType); - this._proxy.$unregisterEditorProvider(viewType); - }); + return VSCodeDisposable.from( + disposable, + new VSCodeDisposable(() => { + this._proxy.$unregisterEditorProvider(viewType); + })); } public $onMessage( @@ -421,6 +628,42 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { await serializer.deserializeWebviewPanel(revivedPanel, state); } + async $createWebviewCustomEditorDocument(resource: UriComponents, viewType: string) { + const entry = this._editorProviders.get(viewType); + if (!entry) { + throw new Error(`No provider found for '${viewType}'`); + } + + if (entry.type !== WebviewEditorType.Custom) { + throw new Error(`Invalid provide type for '${viewType}'`); + } + + const revivedResource = URI.revive(resource); + const document = CustomDocument.create(this._proxy, viewType, revivedResource); + const capabilities = await entry.provider.resolveCustomDocument(document); + document._setCapabilities(capabilities); + this._documents.add(document); + return { + editable: !!capabilities.editing + }; + } + + async $disposeWebviewCustomEditorDocument(resource: UriComponents, viewType: string): Promise { + const entry = this._editorProviders.get(viewType); + if (!entry) { + throw new Error(`No provider found for '${viewType}'`); + } + + if (entry.type !== WebviewEditorType.Custom) { + throw new Error(`Invalid provider type for '${viewType}'`); + } + + const revivedResource = URI.revive(resource); + const document = this.getDocument(viewType, revivedResource); + this._documents.delete(document); + document.dispose(); + } + async $resolveWebviewEditor( resource: UriComponents, handle: WebviewPanelHandle, @@ -431,81 +674,79 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { ): Promise { const entry = this._editorProviders.get(viewType); if (!entry) { - return Promise.reject(new Error(`No provider found for '${viewType}'`)); + throw new Error(`No provider found for '${viewType}'`); } - const { provider, extension } = entry; - const webview = new ExtHostWebview(handle, this._proxy, options, this.initData, this.workspace, extension, this._deprecationService); + const webview = new ExtHostWebview(handle, this._proxy, options, this.initData, this.workspace, entry.extension, this._deprecationService); const revivedPanel = new ExtHostWebviewEditor(handle, this._proxy, viewType, title, typeof position === 'number' && position >= 0 ? typeConverters.ViewColumn.to(position) : undefined, options, webview); this._webviewPanels.set(handle, revivedPanel); + const revivedResource = URI.revive(resource); - await provider.resolveWebviewEditor(revivedResource, revivedPanel); - } - $undoEdits(resourceComponents: UriComponents, viewType: string, editIds: readonly number[]): void { - const provider = this.getEditorProvider(viewType); - if (!provider?.editingDelegate) { - return; - } - - const resource = URI.revive(resourceComponents); - const edits = editIds.map(id => this._edits.get(id, 0)); - provider.editingDelegate.undoEdits(resource, edits); - } - - $applyEdits(resourceComponents: UriComponents, viewType: string, editIds: readonly number[]): void { - const provider = this.getEditorProvider(viewType); - if (!provider?.editingDelegate) { - return; - } - - const resource = URI.revive(resourceComponents); - const edits = editIds.map(id => this._edits.get(id, 0)); - provider.editingDelegate.applyEdits(resource, edits); - } - - $disposeEdits(editIds: readonly number[]): void { - for (const edit of editIds) { - this._edits.delete(edit); + switch (entry.type) { + case WebviewEditorType.Custom: + { + const document = this.getDocument(viewType, revivedResource); + return entry.provider.resolveCustomEditor(document, revivedPanel); + } + case WebviewEditorType.Text: + { + await this._extHostDocuments.ensureDocumentData(revivedResource); + const document = this._extHostDocuments.getDocument(revivedResource); + return entry.provider.resolveCustomTextEditor(document, revivedPanel); + } + default: + { + throw new Error('Unknown webview provider type'); + } } } - async $onSave(resource: UriComponents, viewType: string): Promise { - const provider = this.getEditorProvider(viewType); - return provider?.editingDelegate?.save(URI.revive(resource)); + async $undo(resourceComponents: UriComponents, viewType: string): Promise { + const document = this.getDocument(viewType, resourceComponents); + document._undo(); } - async $onSaveAs(resource: UriComponents, viewType: string, targetResource: UriComponents): Promise { - const provider = this.getEditorProvider(viewType); - return provider?.editingDelegate?.saveAs(URI.revive(resource), URI.revive(targetResource)); + async $redo(resourceComponents: UriComponents, viewType: string): Promise { + const document = this.getDocument(viewType, resourceComponents); + document._redo(); } - async $backup(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise { - const provider = this.getEditorProvider(viewType); - if (!provider?.editingDelegate?.backup) { - return false; - } - return provider.editingDelegate.backup(URI.revive(resource), cancellation); + async $revert(resourceComponents: UriComponents, viewType: string): Promise { + const document = this.getDocument(viewType, resourceComponents); + document._revert(); + } + + async $onSave(resourceComponents: UriComponents, viewType: string): Promise { + const document = this.getDocument(viewType, resourceComponents); + document._save(); + } + + async $onSaveAs(resourceComponents: UriComponents, viewType: string, targetResource: UriComponents): Promise { + const document = this.getDocument(viewType, resourceComponents); + return document._saveAs(URI.revive(targetResource)); + } + + async $backup(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise { + const document = this.getDocument(viewType, resourceComponents); + return document._backup(cancellation); } private getWebviewPanel(handle: WebviewPanelHandle): ExtHostWebviewEditor | undefined { return this._webviewPanels.get(handle); } - private getEditorProvider(viewType: string): vscode.WebviewCustomEditorProvider | undefined { - return this._editorProviders.get(viewType)?.provider; + private getDocument(viewType: string, resource: UriComponents): CustomDocument { + const document = this._documents.get(viewType, URI.revive(resource)); + if (!document) { + throw new Error('No webview editor custom document found'); + } + return document; } +} - private getCapabilites(capabilities: vscode.WebviewCustomEditorProvider) { - const declaredCapabilites: WebviewEditorCapabilities[] = []; - if (capabilities.editingDelegate) { - declaredCapabilites.push(WebviewEditorCapabilities.Editable); - } - if (capabilities.editingDelegate?.backup) { - declaredCapabilites.push(WebviewEditorCapabilities.SupportsHotExit); - } - return declaredCapabilites; - } +function toExtensionData(extension: IExtensionDescription): WebviewExtensionDescription { + return { id: extension.identifier, location: extension.extensionLocation }; } function convertWebviewOptions( diff --git a/src/vs/workbench/api/common/jsonValidationExtensionPoint.ts b/src/vs/workbench/api/common/jsonValidationExtensionPoint.ts index 66b60ca3743..c3f09c4cdb1 100644 --- a/src/vs/workbench/api/common/jsonValidationExtensionPoint.ts +++ b/src/vs/workbench/api/common/jsonValidationExtensionPoint.ts @@ -69,8 +69,8 @@ export class JSONValidationExtensionPoint { } catch (e) { collector.error(nls.localize('invalid.url.fileschema', "'configuration.jsonValidation.url' is an invalid relative URL: {0}", e.message)); } - } else if (!strings.startsWith(uri, 'https:/') && strings.startsWith(uri, 'https:/')) { - collector.error(nls.localize('invalid.url.schema', "'configuration.jsonValidation.url' must start with 'http:', 'https:' or './' to reference schemas located in the extension")); + } else if (!/^[^:/?#]+:\/\//.test(uri)) { + collector.error(nls.localize('invalid.url.schema', "'configuration.jsonValidation.url' must be an absolute URL or start with './' to reference schemas located in the extension.")); return; } }); diff --git a/src/vs/workbench/api/common/shared/tasks.ts b/src/vs/workbench/api/common/shared/tasks.ts index 465a042d8b5..17396177e5b 100644 --- a/src/vs/workbench/api/common/shared/tasks.ts +++ b/src/vs/workbench/api/common/shared/tasks.ts @@ -78,7 +78,7 @@ export interface TaskSourceDTO { export interface TaskHandleDTO { id: string; - workspaceFolder: UriComponents; + workspaceFolder: UriComponents | string; } export interface TaskDTO { diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index 73f89d7a850..f03e93dd95f 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -21,10 +21,14 @@ import { isWindows, isLinux, isWeb } from 'vs/base/common/platform'; import { IsMacNativeContext } from 'vs/workbench/browser/contextkeys'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { InEditorZenModeContext, IsCenteredLayoutContext, EditorAreaVisibleContext } from 'vs/workbench/common/editor'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { SideBarVisibleContext } from 'vs/workbench/common/viewlet'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IViewDescriptorService, IViewContainersRegistry, Extensions as ViewContainerExtensions } from 'vs/workbench/common/views'; +import { IViewDescriptorService, IViewContainersRegistry, Extensions as ViewContainerExtensions, IViewsService, FocusedViewContext, ViewContainerLocation } from 'vs/workbench/common/views'; +import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; +import { IActivityBarService } from 'vs/workbench/services/activityBar/browser/activityBarService'; const registry = Registry.as(WorkbenchExtensions.WorkbenchActions); const viewCategory = nls.localize('view', "View"); @@ -518,6 +522,99 @@ export class ResetViewLocationsAction extends Action { registry.registerWorkbenchAction(SyncActionDescriptor.create(ResetViewLocationsAction, ResetViewLocationsAction.ID, ResetViewLocationsAction.LABEL), 'View: Reset View Locations', viewCategory); +// --- Move View with Command +export class MoveFocusedViewAction extends Action { + static readonly ID = 'workbench.action.moveFocusedView'; + static readonly LABEL = nls.localize('moveFocusedView', "Move Focused View"); + + constructor( + id: string, + label: string, + @IViewDescriptorService private viewDescriptorService: IViewDescriptorService, + @IViewsService private viewsService: IViewsService, + @IQuickInputService private quickInputService: IQuickInputService, + @IContextKeyService private contextKeyService: IContextKeyService, + @INotificationService private notificationService: INotificationService, + @IActivityBarService private activityBarService: IActivityBarService, + @IViewletService private viewletService: IViewletService + ) { + super(id, label); + } + + run(): Promise { + const viewContainerRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); + + const focusedViewId = FocusedViewContext.getValue(this.contextKeyService); + + if (focusedViewId === undefined || focusedViewId.trim() === '') { + this.notificationService.error(nls.localize('moveFocusedView.error.noFocusedView', "There is no view currently focused.")); + return Promise.resolve(); + } + + const viewDescriptor = this.viewDescriptorService.getViewDescriptor(focusedViewId); + if (!viewDescriptor || !viewDescriptor.canMoveView) { + this.notificationService.error(nls.localize('moveFocusedView.error.nonMovableView', "The currently focused view is not movable {0}.", focusedViewId)); + return Promise.resolve(); + } + + const quickPick = this.quickInputService.createQuickPick(); + quickPick.placeholder = nls.localize('moveFocusedView.selectDestination', "Select a Destination for the View"); + + const pinnedViewlets = this.activityBarService.getPinnedViewletIds(); + const items: Array = this.viewletService.getViewlets() + .filter(viewlet => { + if (viewlet.id === this.viewDescriptorService.getViewContainer(focusedViewId)!.id) { + return false; + } + + return !viewContainerRegistry.get(viewlet.id)!.rejectAddedViews && pinnedViewlets.indexOf(viewlet.id) !== -1; + }) + .map(viewlet => { + return { + id: viewlet.id, + label: viewlet.name, + }; + }); + + if (this.viewDescriptorService.getViewLocation(focusedViewId) !== ViewContainerLocation.Panel) { + items.unshift({ + type: 'separator', + label: nls.localize('sidebar', "Side Bar") + }); + items.push({ + type: 'separator', + label: nls.localize('panel', "Panel") + }); + items.push({ + id: '_.panel.newcontainer', + label: nls.localize('moveFocusedView.newContainerInPanel', "New Container in Panel"), + }); + } + + quickPick.items = items; + + quickPick.onDidAccept(() => { + const destination = quickPick.selectedItems[0]; + + if (destination.id === '_.panel.newcontainer') { + this.viewDescriptorService.moveViewToLocation(viewDescriptor!, ViewContainerLocation.Panel); + this.viewsService.openView(focusedViewId, true); + } else if (destination.id) { + this.viewDescriptorService.moveViewsToContainer([viewDescriptor], viewContainerRegistry.get(destination.id)!); + this.viewsService.openView(focusedViewId, true); + } + + quickPick.hide(); + }); + + quickPick.show(); + + return Promise.resolve(); + } +} + +registry.registerWorkbenchAction(SyncActionDescriptor.create(MoveFocusedViewAction, MoveFocusedViewAction.ID, MoveFocusedViewAction.LABEL), 'View: Move Focused View', viewCategory); + // --- Resize View diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index 1d346c89ff3..eece5353caa 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -4,18 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; -import * as resources from 'vs/base/common/resources'; +import { dirname, isEqual, basenameOrAuthority } from 'vs/base/common/resources'; import { IconLabel, IIconLabelValueOptions, IIconLabelCreationOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IModeService } from 'vs/editor/common/services/modeService'; -import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IDecorationsService, IResourceDecorationChangeEvent } from 'vs/workbench/services/decorations/browser/decorations'; import { Schemas } from 'vs/base/common/network'; -import { FileKind, FILES_ASSOCIATIONS_CONFIG, IFileService } from 'vs/platform/files/common/files'; +import { FileKind, FILES_ASSOCIATIONS_CONFIG } from 'vs/platform/files/common/files'; import { ITextModel } from 'vs/editor/common/model'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { Event, Emitter } from 'vs/base/common/event'; @@ -88,7 +87,6 @@ export class ResourceLabels extends Disposable { @IModelService private readonly modelService: IModelService, @IDecorationsService private readonly decorationsService: IDecorationsService, @IThemeService private readonly themeService: IThemeService, - @IFileService private readonly fileService: IFileService, @ILabelService private readonly labelService: ILabelService, @ITextFileService private readonly textFileService: ITextFileService ) { @@ -113,10 +111,6 @@ export class ResourceLabels extends Disposable { return; // we need the resource to compare } - if (this.fileService.canHandleResource(e.model.uri) && e.oldModeId === PLAINTEXT_MODE_ID) { - return; // ignore transitions in files from no mode to specific mode because this happens each time a model is created - } - this._widgets.forEach(widget => widget.notifyModelModeChanged(e.model)); })); @@ -217,11 +211,10 @@ export class ResourceLabel extends ResourceLabels { @IModelService modelService: IModelService, @IDecorationsService decorationsService: IDecorationsService, @IThemeService themeService: IThemeService, - @IFileService fileService: IFileService, @ILabelService labelService: ILabelService, @ITextFileService textFileService: ITextFileService ) { - super(DEFAULT_LABELS_CONTAINER, instantiationService, extensionService, configurationService, modelService, decorationsService, themeService, fileService, labelService, textFileService); + super(DEFAULT_LABELS_CONTAINER, instantiationService, extensionService, configurationService, modelService, decorationsService, themeService, labelService, textFileService); this._label = this._register(this.create(container, options)); } @@ -323,7 +316,7 @@ class ResourceLabelWidget extends IconLabel { } notifyUntitledLabelChange(resource: URI): void { - if (resources.isEqual(resource, this.label?.resource)) { + if (isEqual(resource, this.label?.resource)) { this.render(false); } } @@ -340,19 +333,19 @@ class ResourceLabelWidget extends IconLabel { } if (!name) { - name = resources.basenameOrAuthority(resource); + name = basenameOrAuthority(resource); } } let description: string | undefined; if (!options?.hidePath) { - description = this.labelService.getUriLabel(resources.dirname(resource), { relative: true }); + description = this.labelService.getUriLabel(dirname(resource), { relative: true }); } this.setResource({ resource, name, description }, options); } - setResource(label: IResourceLabelProps, options?: IResourceLabelOptions): void { + setResource(label: IResourceLabelProps, options: IResourceLabelOptions = Object.create(null)): void { if (label.resource?.scheme === Schemas.untitled) { // Untitled labels are very dynamic because they may change // whenever the content changes (unless a path is associated). @@ -368,17 +361,20 @@ class ResourceLabelWidget extends IconLabel { } if (typeof label.description === 'string') { - let untitledDescription: string; - if (untitledModel.hasAssociatedFilePath) { - untitledDescription = this.labelService.getUriLabel(resources.dirname(untitledModel.resource), { relative: true }); - } else { - untitledDescription = untitledModel.resource.path; - } - + let untitledDescription = untitledModel.resource.path; if (label.name !== untitledDescription) { label.description = untitledDescription; + } else { + label.description = undefined; } } + + let untitledTitle = untitledModel.resource.path; + if (untitledModel.name !== untitledTitle) { + options.title = `${untitledModel.name} • ${untitledTitle}`; + } else { + options.title = untitledTitle; + } } } diff --git a/src/vs/workbench/browser/parts/compositeBar.ts b/src/vs/workbench/browser/parts/compositeBar.ts index 3d4807ced1d..f7f3e18a1b3 100644 --- a/src/vs/workbench/browser/parts/compositeBar.ts +++ b/src/vs/workbench/browser/parts/compositeBar.ts @@ -52,20 +52,28 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop { if (targetCompositeId) { if (currentLocation !== this.targetContainerLocation && this.targetContainerLocation !== ViewContainerLocation.Panel) { const destinationContainer = viewContainerRegistry.get(targetCompositeId); - if (destinationContainer) { + if (destinationContainer && !destinationContainer.rejectAddedViews) { this.viewDescriptorService.moveViewsToContainer(this.viewDescriptorService.getViewDescriptors(currentContainer)!.allViewDescriptors.filter(vd => vd.canMoveView), destinationContainer); this.openComposite(targetCompositeId, true); } } else { this.moveComposite(dragData.id, targetCompositeId); } + } else { + const draggedViews = this.viewDescriptorService.getViewDescriptors(currentContainer).allViewDescriptors; + if (draggedViews.length === 1 && draggedViews[0].canMoveView) { + dragData.type = 'view'; + dragData.id = draggedViews[0].id; + } } - } else { + } + + if (dragData.type === 'view') { const viewDescriptor = this.viewDescriptorService.getViewDescriptor(dragData.id); if (viewDescriptor && viewDescriptor.canMoveView) { if (targetCompositeId) { const destinationContainer = viewContainerRegistry.get(targetCompositeId); - if (destinationContainer) { + if (destinationContainer && !destinationContainer.rejectAddedViews) { if (this.targetContainerLocation === ViewContainerLocation.Sidebar) { this.viewDescriptorService.moveViewsToContainer([viewDescriptor], destinationContainer); this.openComposite(targetCompositeId, true); @@ -105,13 +113,28 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop { // ... across view containers but without a destination composite if (!targetCompositeId) { - return false; + const draggedViews = this.viewDescriptorService.getViewDescriptors(currentContainer)!.allViewDescriptors; + if (draggedViews.some(vd => !vd.canMoveView)) { + return false; + } + + if (draggedViews.length !== 1) { + return false; + } + + const defaultLocation = viewContainerRegistry.getViewContainerLocation(this.viewDescriptorService.getDefaultContainer(draggedViews[0].id)!); + if (this.targetContainerLocation === ViewContainerLocation.Sidebar && this.targetContainerLocation !== defaultLocation) { + return false; + } + + return true; } // ... from panel to the sidebar if (this.targetContainerLocation === ViewContainerLocation.Sidebar) { const destinationContainer = viewContainerRegistry.get(targetCompositeId); return !!destinationContainer && + !destinationContainer.rejectAddedViews && this.viewDescriptorService.getViewDescriptors(currentContainer)!.allViewDescriptors.some(vd => vd.canMoveView); } // ... from sidebar to the panel @@ -133,7 +156,8 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop { } // ... into a destination - return true; + const destinationContainer = viewContainerRegistry.get(targetCompositeId); + return !!destinationContainer && !destinationContainer.rejectAddedViews; } return false; @@ -241,10 +265,7 @@ export class CompositeBar extends Widget implements ICompositeBar { const draggedCompositeId = data[0].id; this.compositeTransfer.clearData(DraggedCompositeIdentifier.prototype); - const targetItem = this.model.visibleItems[this.model.visibleItems.length - 1]; - if (targetItem && targetItem.id !== draggedCompositeId) { - this.move(draggedCompositeId, targetItem.id); - } + this.options.dndHandler.drop(new CompositeDragAndDropData('composite', draggedCompositeId), undefined, e); } } diff --git a/src/vs/workbench/browser/parts/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositeBarActions.ts index f09ef9d3c3a..c4eb0fbc8ed 100644 --- a/src/vs/workbench/browser/parts/compositeBarActions.ts +++ b/src/vs/workbench/browser/parts/compositeBarActions.ts @@ -547,6 +547,13 @@ export class CompositeActionViewItem extends ActivityActionViewItem { this.updateFromDragging(container, true); } } + + if (this.compositeTransfer.hasData(DraggedViewIdentifier.prototype)) { + const data = this.compositeTransfer.getData(DraggedViewIdentifier.prototype); + if (Array.isArray(data) && data[0].id !== this.activity.id) { + this.updateFromDragging(container, true); + } + } }, onDragOver: e => { @@ -575,7 +582,8 @@ export class CompositeActionViewItem extends ActivityActionViewItem { }, onDragLeave: e => { - if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype)) { + if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype) || + this.compositeTransfer.hasData(DraggedViewIdentifier.prototype)) { this.updateFromDragging(container, false); } }, @@ -608,6 +616,8 @@ export class CompositeActionViewItem extends ActivityActionViewItem { const data = this.compositeTransfer.getData(DraggedViewIdentifier.prototype); if (Array.isArray(data)) { const draggedViewId = data[0].id; + this.updateFromDragging(container, false); + this.compositeTransfer.clearData(DraggedViewIdentifier.prototype); this.dndHandler.drop(new CompositeDragAndDropData('view', draggedViewId), this.activity.id, e); } diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index b46b49c60ff..27db5400aa3 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -59,6 +59,7 @@ export abstract class CompositePart extends Part { protected readonly onDidCompositeClose = this._register(new Emitter()); protected toolBar: ToolBar | undefined; + protected titleLabelElement: HTMLElement | undefined; private mapCompositeToCompositeContainer = new Map(); private mapActionsBindingToComposite = new Map void>(); @@ -402,6 +403,7 @@ export abstract class CompositePart extends Part { protected createTitleLabel(parent: HTMLElement): ICompositeTitleLabel { const titleContainer = append(parent, $('.title-label')); const titleLabel = append(titleContainer, $('h2')); + this.titleLabelElement = titleLabel; const $this = this; return { diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index 5aeb9a3efba..4d1f372052f 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -642,14 +642,13 @@ export abstract class BaseCloseAllAction extends Action { return; } - let saveOrRevert: boolean; if (confirm === ConfirmResult.DONT_SAVE) { - saveOrRevert = await this.editorService.revertAll({ soft: true, includeUntitled: true }); + await this.editorService.revertAll({ soft: true, includeUntitled: true }); } else { - saveOrRevert = await this.editorService.saveAll({ reason: SaveReason.EXPLICIT, includeUntitled: true }); + await this.editorService.saveAll({ reason: SaveReason.EXPLICIT, includeUntitled: true }); } - if (saveOrRevert) { + if (!this.workingCopyService.hasDirty) { return this.doCloseAll(); } } diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index 64e258c60b0..34ae3b128ab 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -144,6 +144,7 @@ class StateChange { encoding: boolean = false; EOL: boolean = false; tabFocusMode: boolean = false; + columnSelectionMode: boolean = false; screenReaderMode: boolean = false; metadata: boolean = false; @@ -154,6 +155,7 @@ class StateChange { this.encoding = this.encoding || other.encoding; this.EOL = this.EOL || other.EOL; this.tabFocusMode = this.tabFocusMode || other.tabFocusMode; + this.columnSelectionMode = this.columnSelectionMode || other.columnSelectionMode; this.screenReaderMode = this.screenReaderMode || other.screenReaderMode; this.metadata = this.metadata || other.metadata; } @@ -165,21 +167,23 @@ class StateChange { || this.encoding || this.EOL || this.tabFocusMode + || this.columnSelectionMode || this.screenReaderMode || this.metadata; } } -interface StateDelta { - selectionStatus?: string; - mode?: string; - encoding?: string; - EOL?: string; - indentation?: string; - tabFocusMode?: boolean; - screenReaderMode?: boolean; - metadata?: string | undefined; -} +type StateDelta = ( + { type: 'selectionStatus'; selectionStatus: string | undefined; } + | { type: 'mode'; mode: string | undefined; } + | { type: 'encoding'; encoding: string | undefined; } + | { type: 'EOL'; EOL: string | undefined; } + | { type: 'indentation'; indentation: string | undefined; } + | { type: 'tabFocusMode'; tabFocusMode: boolean; } + | { type: 'columnSelectionMode'; columnSelectionMode: boolean; } + | { type: 'screenReaderMode'; screenReaderMode: boolean; } + | { type: 'metadata'; metadata: string | undefined; } +); class State { @@ -201,6 +205,9 @@ class State { private _tabFocusMode: boolean | undefined; get tabFocusMode(): boolean | undefined { return this._tabFocusMode; } + private _columnSelectionMode: boolean | undefined; + get columnSelectionMode(): boolean | undefined { return this._columnSelectionMode; } + private _screenReaderMode: boolean | undefined; get screenReaderMode(): boolean | undefined { return this._screenReaderMode; } @@ -210,56 +217,63 @@ class State { update(update: StateDelta): StateChange { const change = new StateChange(); - if ('selectionStatus' in update) { + if (update.type === 'selectionStatus') { if (this._selectionStatus !== update.selectionStatus) { this._selectionStatus = update.selectionStatus; change.selectionStatus = true; } } - if ('indentation' in update) { + if (update.type === 'indentation') { if (this._indentation !== update.indentation) { this._indentation = update.indentation; change.indentation = true; } } - if ('mode' in update) { + if (update.type === 'mode') { if (this._mode !== update.mode) { this._mode = update.mode; change.mode = true; } } - if ('encoding' in update) { + if (update.type === 'encoding') { if (this._encoding !== update.encoding) { this._encoding = update.encoding; change.encoding = true; } } - if ('EOL' in update) { + if (update.type === 'EOL') { if (this._EOL !== update.EOL) { this._EOL = update.EOL; change.EOL = true; } } - if ('tabFocusMode' in update) { + if (update.type === 'tabFocusMode') { if (this._tabFocusMode !== update.tabFocusMode) { this._tabFocusMode = update.tabFocusMode; change.tabFocusMode = true; } } - if ('screenReaderMode' in update) { + if (update.type === 'columnSelectionMode') { + if (this._columnSelectionMode !== update.columnSelectionMode) { + this._columnSelectionMode = update.columnSelectionMode; + change.columnSelectionMode = true; + } + } + + if (update.type === 'screenReaderMode') { if (this._screenReaderMode !== update.screenReaderMode) { this._screenReaderMode = update.screenReaderMode; change.screenReaderMode = true; } } - if ('metadata' in update) { + if (update.type === 'metadata') { if (this._metadata !== update.metadata) { this._metadata = update.metadata; change.metadata = true; @@ -279,6 +293,7 @@ const nlsEOLCRLF = nls.localize('endOfLineCarriageReturnLineFeed', "CRLF"); export class EditorStatus extends Disposable implements IWorkbenchContribution { private readonly tabFocusModeElement = this._register(new MutableDisposable()); + private readonly columnSelectionModeElement = this._register(new MutableDisposable()); private readonly screenRedearModeElement = this._register(new MutableDisposable()); private readonly indentationElement = this._register(new MutableDisposable()); private readonly selectionElement = this._register(new MutableDisposable()); @@ -399,6 +414,22 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { } } + private updateColumnSelectionModeElement(visible: boolean): void { + if (visible) { + if (!this.columnSelectionModeElement.value) { + this.columnSelectionModeElement.value = this.statusbarService.addEntry({ + text: nls.localize('columnSelectionModeEnabled', "Column Selection"), + tooltip: nls.localize('disableColumnSelectionMode', "Disable Column Selection Mode"), + command: 'editor.action.toggleColumnSelection', + backgroundColor: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_BACKGROUND), + color: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_FOREGROUND) + }, 'status.editor.columnSelectionMode', nls.localize('status.editor.columnSelectionMode', "Column Selection Mode"), StatusbarAlignment.RIGHT, 100.8); + } + } else { + this.columnSelectionModeElement.clear(); + } + } + private updateScreenReaderModeElement(visible: boolean): void { if (visible) { if (!this.screenRedearModeElement.value) { @@ -541,6 +572,7 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { private doRenderNow(changed: StateChange): void { this.updateTabFocusModeElement(!!this.state.tabFocusMode); + this.updateColumnSelectionModeElement(!!this.state.columnSelectionMode); this.updateScreenReaderModeElement(!!this.state.screenReaderMode); this.updateIndentationElement(this.state.indentation); this.updateSelectionElement(this.state.selectionStatus && !this.state.screenReaderMode ? this.state.selectionStatus : undefined); @@ -580,6 +612,7 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { const activeCodeEditor = activeControl ? withNullAsUndefined(getCodeEditor(activeControl.getControl())) : undefined; // Update all states + this.onColumnSelectionModeChange(activeCodeEditor); this.onScreenReaderModeChange(activeCodeEditor); this.onSelectionChange(activeCodeEditor); this.onModeChange(activeCodeEditor, activeInput); @@ -597,6 +630,9 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { // Hook Listener for Configuration changes this.activeEditorListeners.add(activeCodeEditor.onDidChangeConfiguration((event: ConfigurationChangedEvent) => { + if (event.hasChanged(EditorOption.columnSelection)) { + this.onColumnSelectionModeChange(activeCodeEditor); + } if (event.hasChanged(EditorOption.accessibilitySupport)) { this.onScreenReaderModeChange(activeCodeEditor); } @@ -665,14 +701,14 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { } private onModeChange(editorWidget: ICodeEditor | undefined, editorInput: IEditorInput | undefined): void { - let info: StateDelta = { mode: undefined }; + let info: StateDelta = { type: 'mode', mode: undefined }; // We only support text based editors if (editorWidget && editorInput && toEditorWithModeSupport(editorInput)) { const textModel = editorWidget.getModel(); if (textModel) { const modeId = textModel.getLanguageIdentifier().language; - info = { mode: withNullAsUndefined(this.modeService.getLanguageName(modeId)) }; + info.mode = withNullAsUndefined(this.modeService.getLanguageName(modeId)); } } @@ -680,7 +716,7 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { } private onIndentationChange(editorWidget: ICodeEditor | undefined): void { - const update: StateDelta = { indentation: undefined }; + const update: StateDelta = { type: 'indentation', indentation: undefined }; if (editorWidget) { const model = editorWidget.getModel(); @@ -698,7 +734,7 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { } private onMetadataChange(editor: IBaseEditor | undefined): void { - const update: StateDelta = { metadata: undefined }; + const update: StateDelta = { type: 'metadata', metadata: undefined }; if (editor instanceof BaseBinaryResourceEditor || editor instanceof BinaryResourceDiffEditor) { update.metadata = editor.getMetadata(); @@ -707,6 +743,16 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { this.updateState(update); } + private onColumnSelectionModeChange(editorWidget: ICodeEditor | undefined): void { + const info: StateDelta = { type: 'columnSelectionMode', columnSelectionMode: false }; + + if (editorWidget && editorWidget.getOption(EditorOption.columnSelection)) { + info.columnSelectionMode = true; + } + + this.updateState(info); + } + private onScreenReaderModeChange(editorWidget: ICodeEditor | undefined): void { let screenReaderMode = false; @@ -730,7 +776,7 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { this.screenReaderNotification.close(); } - this.updateState({ screenReaderMode: screenReaderMode }); + this.updateState({ type: 'screenReaderMode', screenReaderMode: screenReaderMode }); } private onSelectionChange(editorWidget: ICodeEditor | undefined): void { @@ -770,11 +816,11 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { } } - this.updateState({ selectionStatus: this.getSelectionLabel(info) }); + this.updateState({ type: 'selectionStatus', selectionStatus: this.getSelectionLabel(info) }); } private onEOLChange(editorWidget: ICodeEditor | undefined): void { - const info: StateDelta = { EOL: undefined }; + const info: StateDelta = { type: 'EOL', EOL: undefined }; if (editorWidget && !editorWidget.getOption(EditorOption.readOnly)) { const codeEditorModel = editorWidget.getModel(); @@ -791,7 +837,7 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { return; } - const info: StateDelta = { encoding: undefined }; + const info: StateDelta = { type: 'encoding', encoding: undefined }; // We only support text based editors that have a model associated // This ensures we do not show the encoding picker while an editor @@ -825,7 +871,7 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { } private onTabFocusModeChange(): void { - const info: StateDelta = { tabFocusMode: TabFocus.getTabFocusMode() }; + const info: StateDelta = { type: 'tabFocusMode', tabFocusMode: TabFocus.getTabFocusMode() }; this.updateState(info); } @@ -1224,7 +1270,9 @@ export class ChangeEOLAction extends Action { const activeCodeEditor = getCodeEditor(this.editorService.activeTextEditorWidget); if (activeCodeEditor?.hasModel() && !this.editorService.activeEditor?.isReadonly()) { textModel = activeCodeEditor.getModel(); + textModel.pushStackElement(); textModel.pushEOL(eol.eol); + textModel.pushStackElement(); } } } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsActions.ts b/src/vs/workbench/browser/parts/notifications/notificationsActions.ts index f6e1ee43461..82301a0448e 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsActions.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsActions.ts @@ -63,7 +63,7 @@ export class HideNotificationsCenterAction extends Action { label: string, @ICommandService private readonly commandService: ICommandService ) { - super(id, label, 'codicon-close'); + super(id, label, 'codicon-chevron-down'); } run(notification: INotificationViewItem): Promise { diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts index 0b4cbe77264..094c9e1567f 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts @@ -11,7 +11,7 @@ import { INotificationsModel, INotificationChangeEvent, NotificationChangeType } import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { Emitter } from 'vs/base/common/event'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { NotificationsCenterVisibleContext } from 'vs/workbench/browser/parts/notifications/notificationsCommands'; +import { NotificationsCenterVisibleContext, INotificationsCenterController } from 'vs/workbench/browser/parts/notifications/notificationsCommands'; import { NotificationsList } from 'vs/workbench/browser/parts/notifications/notificationsList'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { addClass, removeClass, isAncestor, Dimension } from 'vs/base/browser/dom'; @@ -24,7 +24,7 @@ import { IAction } from 'vs/base/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { assertAllDefined, assertIsDefined } from 'vs/base/common/types'; -export class NotificationsCenter extends Themable { +export class NotificationsCenter extends Themable implements INotificationsCenterController { private static readonly MAX_DIMENSIONS = new Dimension(450, 400); @@ -284,8 +284,10 @@ export class NotificationsCenter extends Themable { this.hide(); // Close all - while (this.model.notifications.length) { - this.model.notifications[0].close(); + for (const notification of this.model.notifications) { + if (!notification.hasProgress) { + notification.close(); + } } } } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts index 133bb8ff3ab..413e700668d 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts @@ -107,7 +107,7 @@ export function registerNotificationCommands(center: INotificationsCenterControl }, handler: (accessor, args?: any) => { const notification = getNotificationFromContext(accessor.get(IListService), args); - if (notification) { + if (notification && !notification.hasProgress) { notification.close(); } } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts b/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts index 3545ab9c02b..1bdde771ccb 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { INotificationsModel, INotificationChangeEvent, NotificationChangeType, INotificationViewItem, IStatusMessageChangeEvent, StatusMessageChangeType, IStatusMessageViewItem } from 'vs/workbench/common/notifications'; +import { INotificationsModel, INotificationChangeEvent, NotificationChangeType, IStatusMessageChangeEvent, StatusMessageChangeType, IStatusMessageViewItem } from 'vs/workbench/common/notifications'; import { IStatusbarService, StatusbarAlignment, IStatusbarEntryAccessor, IStatusbarEntry } from 'vs/workbench/services/statusbar/common/statusbar'; import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { HIDE_NOTIFICATIONS_CENTER, SHOW_NOTIFICATIONS_CENTER } from 'vs/workbench/browser/parts/notifications/notificationsCommands'; @@ -12,11 +12,12 @@ import { localize } from 'vs/nls'; export class NotificationsStatus extends Disposable { private notificationsCenterStatusItem: IStatusbarEntryAccessor | undefined; - private currentNotifications = new Set(); + private newNotificationsCount = 0; private currentStatusMessage: [IStatusMessageViewItem, IDisposable] | undefined; - private isNotificationsCenterVisible: boolean | undefined; + private isNotificationsCenterVisible: boolean = false; + private isNotificationsToastsVisible: boolean = false; constructor( private model: INotificationsModel, @@ -39,39 +40,56 @@ export class NotificationsStatus extends Disposable { } private onDidChangeNotification(e: INotificationChangeEvent): void { - if (this.isNotificationsCenterVisible) { - return; // no change if notification center is visible - } - - // Notification got Added - if (e.kind === NotificationChangeType.ADD) { - this.currentNotifications.add(e.item); - } - - // Notification got Removed - else if (e.kind === NotificationChangeType.REMOVE) { - this.currentNotifications.delete(e.item); + + // Consider a notification as unread as long as it only + // appeared as toast and not in the notification center + if (!this.isNotificationsCenterVisible) { + if (e.kind === NotificationChangeType.ADD) { + this.newNotificationsCount++; + } else if (e.kind === NotificationChangeType.REMOVE && this.newNotificationsCount > 0) { + this.newNotificationsCount--; + } } + // Update in status bar this.updateNotificationsCenterStatusItem(); } private updateNotificationsCenterStatusItem(): void { + + // Figure out how many notifications have progress only if neither + // toasts are visible nor center is visible. In that case we still + // want to give a hint to the user that something is running. + let notificationsInProgress = 0; + if (!this.isNotificationsCenterVisible && !this.isNotificationsToastsVisible) { + for (const notification of this.model.notifications) { + if (notification.hasProgress) { + notificationsInProgress++; + } + } + } + const statusProperties: IStatusbarEntry = { - text: this.currentNotifications.size === 0 ? '$(bell)' : `$(bell) ${this.currentNotifications.size}`, + text: `${this.newNotificationsCount === 0 ? '$(bell)' : '$(bell-dot)'}${notificationsInProgress > 0 ? ' $(sync~spin)' : ''}`, command: this.isNotificationsCenterVisible ? HIDE_NOTIFICATIONS_CENTER : SHOW_NOTIFICATIONS_CENTER, - tooltip: this.getTooltip(), + tooltip: this.getTooltip(notificationsInProgress), showBeak: this.isNotificationsCenterVisible }; if (!this.notificationsCenterStatusItem) { - this.notificationsCenterStatusItem = this.statusbarService.addEntry(statusProperties, 'status.notifications', localize('status.notifications', "Notifications"), StatusbarAlignment.RIGHT, -Number.MAX_VALUE /* towards the far end of the right hand side */); + this.notificationsCenterStatusItem = this.statusbarService.addEntry( + statusProperties, + 'status.notifications', + localize('status.notifications', "Notifications"), + StatusbarAlignment.RIGHT, + -Number.MAX_VALUE /* towards the far end of the right hand side */ + ); } else { this.notificationsCenterStatusItem.update(statusProperties); } } - private getTooltip(): string { + private getTooltip(notificationsInProgress: number): string { if (this.isNotificationsCenterVisible) { return localize('hideNotifications', "Hide Notifications"); } @@ -80,23 +98,45 @@ export class NotificationsStatus extends Disposable { return localize('zeroNotifications', "No Notifications"); } - if (this.currentNotifications.size === 0) { - return localize('noNotifications', "No New Notifications"); + if (notificationsInProgress === 0) { + if (this.newNotificationsCount === 0) { + return localize('noNotifications', "No New Notifications"); + } + + if (this.newNotificationsCount === 1) { + return localize('oneNotification', "1 New Notification"); + } + + return localize('notifications', "{0} New Notifications", this.newNotificationsCount); } - if (this.currentNotifications.size === 1) { - return localize('oneNotification', "1 New Notification"); + if (this.newNotificationsCount === 0) { + return localize('noNotificationsWithProgress', "No New Notifications ({0} in progress)", notificationsInProgress); } - return localize('notifications', "{0} New Notifications", this.currentNotifications.size); + if (this.newNotificationsCount === 1) { + return localize('oneNotificationWithProgress', "1 New Notification ({0} in progress)", notificationsInProgress); + } + + return localize('notificationsWithProgress', "{0} New Notifications ({0} in progress)", this.newNotificationsCount, notificationsInProgress); } - update(isCenterVisible: boolean): void { + update(isCenterVisible: boolean, isToastsVisible: boolean): void { + let updateNotificationsCenterStatusItem = false; + if (this.isNotificationsCenterVisible !== isCenterVisible) { this.isNotificationsCenterVisible = isCenterVisible; + this.newNotificationsCount = 0; // Showing the notification center resets the unread counter to 0 + updateNotificationsCenterStatusItem = true; + } - // Showing the notification center resets the counter to 0 - this.currentNotifications.clear(); + if (this.isNotificationsToastsVisible !== isToastsVisible) { + this.isNotificationsToastsVisible = isToastsVisible; + updateNotificationsCenterStatusItem = true; + } + + // Update in status bar as needed + if (updateNotificationsCenterStatusItem) { this.updateNotificationsCenterStatusItem(); } } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts index 9461b4887ac..95e287fbd49 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts @@ -9,13 +9,13 @@ import { IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/com import { addClass, removeClass, isAncestor, addDisposableListener, EventType, Dimension } from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { NotificationsList } from 'vs/workbench/browser/parts/notifications/notificationsList'; -import { Event } from 'vs/base/common/event'; +import { Event, Emitter } from 'vs/base/common/event'; import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { Themable, NOTIFICATIONS_TOAST_BORDER, NOTIFICATIONS_BACKGROUND } from 'vs/workbench/common/theme'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { widgetShadow } from 'vs/platform/theme/common/colorRegistry'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { NotificationsToastsVisibleContext } from 'vs/workbench/browser/parts/notifications/notificationsCommands'; +import { NotificationsToastsVisibleContext, INotificationsToastController } from 'vs/workbench/browser/parts/notifications/notificationsCommands'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { localize } from 'vs/nls'; import { Severity, NotificationsFilter } from 'vs/platform/notification/common/notification'; @@ -39,7 +39,7 @@ enum ToastVisibility { VISIBLE } -export class NotificationsToasts extends Themable { +export class NotificationsToasts extends Themable implements INotificationsToastController { private static readonly MAX_WIDTH = 450; private static readonly MAX_NOTIFICATIONS = 3; @@ -53,6 +53,12 @@ export class NotificationsToasts extends Themable { return intervals; })(); + private readonly _onDidChangeVisibility = this._register(new Emitter()); + readonly onDidChangeVisibility = this._onDidChangeVisibility.event; + + private _isVisible = false; + get isVisible(): boolean { return !!this._isVisible; } + private notificationsToastsContainer: HTMLElement | undefined; private workbenchDimensions: Dimension | undefined; private isNotificationsCenterVisible: boolean | undefined; @@ -125,11 +131,11 @@ export class NotificationsToasts extends Themable { private addToast(item: INotificationViewItem): void { if (this.isNotificationsCenterVisible) { - return; // do not show toasts while notification center is visibles + return; // do not show toasts while notification center is visible } if (item.silent) { - return; // do not show toats for silenced notifications + return; // do not show toasts for silenced notifications } // Lazily create toasts containers @@ -174,7 +180,7 @@ export class NotificationsToasts extends Themable { this.mapNotificationToToast.set(item, toast); itemDisposables.add(toDisposable(() => { - if (this.isVisible(toast) && notificationsToastsContainer) { + if (this.isToastVisible(toast) && notificationsToastsContainer) { notificationsToastsContainer.removeChild(toast.container); } })); @@ -229,6 +235,12 @@ export class NotificationsToasts extends Themable { removeClass(notificationToast, 'notification-fade-in'); addClass(notificationToast, 'notification-fade-in-done'); })); + + // Events + if (!this._isVisible) { + this._isVisible = true; + this._onDidChangeVisibility.fire(); + } } private purgeNotification(item: INotificationViewItem, notificationToastContainer: HTMLElement, notificationList: NotificationsList, disposables: DisposableStore): void { @@ -325,6 +337,12 @@ export class NotificationsToasts extends Themable { // Context Key this.notificationsToastsVisibleContextKey.set(false); + + // Events + if (this._isVisible) { + this._isVisible = false; + this._onDidChangeVisibility.fire(); + } } hide(): void { @@ -441,12 +459,12 @@ export class NotificationsToasts extends Themable { notificationToasts.push(toast); break; case ToastVisibility.HIDDEN: - if (!this.isVisible(toast)) { + if (!this.isToastVisible(toast)) { notificationToasts.push(toast); } break; case ToastVisibility.VISIBLE: - if (this.isVisible(toast)) { + if (this.isToastVisible(toast)) { notificationToasts.push(toast); } break; @@ -534,7 +552,7 @@ export class NotificationsToasts extends Themable { } private setVisibility(toast: INotificationToast, visible: boolean): void { - if (this.isVisible(toast) === visible) { + if (this.isToastVisible(toast) === visible) { return; } @@ -546,7 +564,7 @@ export class NotificationsToasts extends Themable { } } - private isVisible(toast: INotificationToast): boolean { + private isToastVisible(toast: INotificationToast): boolean { return !!toast.container.parentElement; } } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index 041863ea737..41063188c35 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -306,7 +306,7 @@ export class NotificationTemplateRenderer extends Disposable { // Container toggleClass(this.template.container, 'expanded', notification.expanded); this.inputDisposables.add(addDisposableListener(this.template.container, EventType.MOUSE_UP, e => { - if (e.button === 1 /* Middle Button */) { + if (!notification.hasProgress && e.button === 1 /* Middle Button */) { EventHelper.stop(e); notification.close(); @@ -403,8 +403,10 @@ export class NotificationTemplateRenderer extends Disposable { actions.push(notification.expanded ? NotificationTemplateRenderer.collapseNotificationAction : NotificationTemplateRenderer.expandNotificationAction); } - // Close - actions.push(NotificationTemplateRenderer.closeNotificationAction); + // Close (unless progress is showing) + if (!notification.hasProgress) { + actions.push(NotificationTemplateRenderer.closeNotificationAction); + } this.template.toolbar.clear(); this.template.toolbar.context = notification; @@ -453,7 +455,7 @@ export class NotificationTemplateRenderer extends Disposable { private renderProgress(notification: INotificationViewItem): void { // Return early if the item has no progress - if (!notification.hasProgress()) { + if (!notification.hasProgress) { this.template.progress.stop().hide(); return; diff --git a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts index 72ec6b5eb82..02a5769f116 100644 --- a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts +++ b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts @@ -33,6 +33,9 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { LayoutPriority } from 'vs/base/browser/ui/grid/grid'; import { assertIsDefined } from 'vs/base/common/types'; +import { LocalSelectionTransfer } from 'vs/workbench/browser/dnd'; +import { DraggedViewIdentifier } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { DraggedCompositeIdentifier } from 'vs/workbench/browser/parts/compositeBarActions'; export class SidebarPart extends CompositePart implements IViewletService { @@ -163,6 +166,29 @@ export class SidebarPart extends CompositePart implements IViewletServi this.onTitleAreaContextMenu(new StandardMouseEvent(e)); })); + this.titleLabelElement!.draggable = true; + this._register(addDisposableListener(this.titleLabelElement!, EventType.DRAG_START, e => { + const activeViewlet = this.getActiveViewlet(); + if (activeViewlet) { + const visibleViews = activeViewlet.getViewPaneContainer().views.filter(v => v.isVisible()); + if (visibleViews.length === 1) { + LocalSelectionTransfer.getInstance().setData([new DraggedViewIdentifier(visibleViews[0].id)], DraggedViewIdentifier.prototype); + } else { + LocalSelectionTransfer.getInstance().setData([new DraggedCompositeIdentifier(activeViewlet.getId())], DraggedCompositeIdentifier.prototype); + } + } + })); + + this._register(addDisposableListener(this.titleLabelElement!, EventType.DRAG_END, e => { + if (LocalSelectionTransfer.getInstance().hasData(DraggedViewIdentifier.prototype)) { + LocalSelectionTransfer.getInstance().clearData(DraggedViewIdentifier.prototype); + } + + if (LocalSelectionTransfer.getInstance().hasData(DraggedCompositeIdentifier.prototype)) { + LocalSelectionTransfer.getInstance().clearData(DraggedCompositeIdentifier.prototype); + } + })); + return titleArea; } diff --git a/src/vs/workbench/browser/parts/views/customView.ts b/src/vs/workbench/browser/parts/views/customView.ts index 32ccfff21ee..e4cfff2a63c 100644 --- a/src/vs/workbench/browser/parts/views/customView.ts +++ b/src/vs/workbench/browser/parts/views/customView.ts @@ -43,6 +43,7 @@ import { CollapseAllAction } from 'vs/base/browser/ui/tree/treeDefaults'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { SIDE_BAR_BACKGROUND, PANEL_BACKGROUND } from 'vs/workbench/common/theme'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; export class CustomTreeViewPane extends ViewPane { @@ -59,8 +60,9 @@ export class CustomTreeViewPane extends ViewPane { @IInstantiationService instantiationService: IInstantiationService, @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, + @ITelemetryService telemetryService: ITelemetryService, ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: options.title, titleMenuId: MenuId.ViewTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: options.title, titleMenuId: MenuId.ViewTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); const { treeView } = (Registry.as(Extensions.ViewsRegistry).getView(options.id)); this.treeView = treeView; this._register(this.treeView.onDidChangeActions(() => this.updateActions(), this)); diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index ec2e7f8df87..bf954530159 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -66,6 +66,10 @@ export class DraggedViewIdentifier { } } +type WelcomeActionClassification = { + viewId: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + uri: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; +}; const viewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); @@ -194,11 +198,12 @@ export abstract class ViewPane extends Pane implements IView { @IKeybindingService protected keybindingService: IKeybindingService, @IContextMenuService protected contextMenuService: IContextMenuService, @IConfigurationService protected readonly configurationService: IConfigurationService, - @IContextKeyService contextKeyService: IContextKeyService, + @IContextKeyService protected contextKeyService: IContextKeyService, @IViewDescriptorService protected viewDescriptorService: IViewDescriptorService, @IInstantiationService protected instantiationService: IInstantiationService, @IOpenerService protected openerService: IOpenerService, @IThemeService protected themeService: IThemeService, + @ITelemetryService protected telemetryService: ITelemetryService, ) { super(options); @@ -250,7 +255,10 @@ export abstract class ViewPane extends Pane implements IView { this._onDidFocus.fire(); })); this._register(focusTracker.onDidBlur(() => { - this.focusedViewContextKey.reset(); + if (this.focusedViewContextKey.get() === this.id) { + this.focusedViewContextKey.reset(); + } + this._onDidBlur.fire(); })); } @@ -397,7 +405,9 @@ export abstract class ViewPane extends Pane implements IView { addClass(this.bodyContainer, 'welcome'); this.viewWelcomeContainer.innerHTML = ''; - for (const { content } of contents) { + let buttonIndex = 0; + + for (const { content, preconditions } of contents) { const lines = content.split('\n'); for (let line of lines) { @@ -416,9 +426,28 @@ export abstract class ViewPane extends Pane implements IView { } else if (linkedText.nodes.length === 1) { const button = new Button(p, { title: node.title }); button.label = node.label; - button.onDidClick(_ => this.openerService.open(node.href), null, disposables); + button.onDidClick(_ => { + this.telemetryService.publicLog2<{ viewId: string, uri: string }, WelcomeActionClassification>('views.welcomeAction', { viewId: this.id, uri: node.href }); + this.openerService.open(node.href); + }, null, disposables); disposables.add(button); disposables.add(attachButtonStyler(button, this.themeService)); + + if (preconditions) { + const precondition = preconditions[buttonIndex]; + + if (precondition) { + const updateEnablement = () => button.enabled = this.contextKeyService.contextMatchesRules(precondition); + updateEnablement(); + + const keys = new Set(); + precondition.keys().forEach(key => keys.add(key)); + const onDidChangeContext = Event.filter(this.contextKeyService.onDidChangeContext, e => e.affectsSome(keys)); + onDidChangeContext(updateEnablement, null, disposables); + } + } + + buttonIndex++; } else { const link = this.instantiationService.createInstance(Link, node); append(p, link.el); diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index acb6715b8d3..2fa180e6968 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -42,6 +42,19 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio key: 'tabDescription' }, "Controls the format of the label for an editor."), }, + 'workbench.editor.untitled.labelFormat': { + 'type': 'string', + 'enum': ['content', 'name'], + 'enumDescriptions': [ + nls.localize('workbench.editor.untitled.labelFormat.content', "The name of the untitled file is derived from the contents of its first line unless it has an associated file path. It will fallback to the name in case the line is empty or contains no word characters."), + nls.localize('workbench.editor.untitled.labelFormat.name', "The name of the untitled file is not derived from the contents of the file."), + ], + 'default': 'content', + 'description': nls.localize({ + comment: ['This is the description for a setting. Values surrounded by parenthesis are not to be translated.'], + key: 'untitledLabelFormat' + }, "Controls the format of the label for an untitled editor."), + }, 'workbench.editor.tabCloseButton': { 'type': 'string', 'enum': ['left', 'right', 'off'], diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index a24435cd241..58afab7134e 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -384,10 +384,14 @@ export class Workbench extends Layout { // Visibility this._register(notificationsCenter.onDidChangeVisibility(() => { - notificationsStatus.update(notificationsCenter.isVisible); + notificationsStatus.update(notificationsCenter.isVisible, notificationsToasts.isVisible); notificationsToasts.update(notificationsCenter.isVisible); })); + this._register(notificationsToasts.onDidChangeVisibility(() => { + notificationsStatus.update(notificationsCenter.isVisible, notificationsToasts.isVisible); + })); + // Register Commands registerNotificationCommands(notificationsCenter, notificationsToasts); } diff --git a/src/vs/workbench/common/actions.ts b/src/vs/workbench/common/actions.ts index 46112bcce7c..7f5a5cbdd14 100644 --- a/src/vs/workbench/common/actions.ts +++ b/src/vs/workbench/common/actions.ts @@ -44,7 +44,10 @@ Registry.add(Extensions.WorkbenchActions, new class implements IWorkbenchActionR KeybindingsRegistry.registerKeybindingRule({ id: descriptor.id, weight: weight, - when: (descriptor.keybindingContext || when ? ContextKeyExpr.and(descriptor.keybindingContext, when) : null), + when: + descriptor.keybindingContext && when + ? ContextKeyExpr.and(descriptor.keybindingContext, when) + : descriptor.keybindingContext || when || null, primary: keybindings ? keybindings.primary : 0, secondary: keybindings?.secondary, win: keybindings?.win, diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 23c66ef5033..66f63a3c0cf 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -363,7 +363,7 @@ export interface IRevertOptions { } export interface IMoveResult { - editor: IEditorInput | IResourceEditor; + editor: EditorInput | IResourceEditor; options?: IEditorOptions; } diff --git a/src/vs/workbench/common/notifications.ts b/src/vs/workbench/common/notifications.ts index ef22a9c992e..73f0ef74053 100644 --- a/src/vs/workbench/common/notifications.ts +++ b/src/vs/workbench/common/notifications.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { INotification, INotificationHandle, INotificationActions, INotificationProgress, NoOpNotification, Severity, NotificationMessage, IPromptChoice, IStatusMessageOptions, NotificationsFilter } from 'vs/platform/notification/common/notification'; +import { INotification, INotificationHandle, INotificationActions, INotificationProgress, NoOpNotification, Severity, NotificationMessage, IPromptChoice, IStatusMessageOptions, NotificationsFilter, INotificationProgressProperties } from 'vs/platform/notification/common/notification'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Event, Emitter } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -261,6 +261,7 @@ export interface INotificationViewItem { readonly expanded: boolean; readonly canCollapse: boolean; + readonly hasProgress: boolean; readonly onDidChangeExpansion: Event; readonly onDidClose: Event; @@ -270,9 +271,6 @@ export interface INotificationViewItem { collapse(skipEvents?: boolean): void; toggle(): void; - hasProgress(): boolean; - hasPrompt(): boolean; - updateSeverity(severity: Severity): void; updateMessage(message: NotificationMessage): void; updateActions(actions?: INotificationActions): void; @@ -437,7 +435,7 @@ export class NotificationViewItem extends Disposable implements INotificationVie actions = { primary: notification.message.actions }; } - return new NotificationViewItem(severity, notification.sticky, notification.silent || filter === NotificationsFilter.SILENT || (filter === NotificationsFilter.ERROR && notification.severity !== Severity.Error), message, notification.source, actions); + return new NotificationViewItem(severity, notification.sticky, notification.silent || filter === NotificationsFilter.SILENT || (filter === NotificationsFilter.ERROR && notification.severity !== Severity.Error), message, notification.source, notification.progress, actions); } private static parseNotificationMessage(input: NotificationMessage): INotificationMessage | undefined { @@ -474,13 +472,30 @@ export class NotificationViewItem extends Disposable implements INotificationVie private _silent: boolean | undefined, private _message: INotificationMessage, private _source: string | undefined, + progress: INotificationProgressProperties | undefined, actions?: INotificationActions ) { super(); + if (progress) { + this.setProgress(progress); + } + this.setActions(actions); } + private setProgress(progress: INotificationProgressProperties): void { + if (progress.infinite) { + this.progress.infinite(); + } else if (progress.total) { + this.progress.total(progress.total); + + if (progress.worked) { + this.progress.worked(progress.worked); + } + } + } + private setActions(actions: INotificationActions = { primary: [], secondary: [] }): void { if (!Array.isArray(actions.primary)) { actions.primary = []; @@ -495,7 +510,7 @@ export class NotificationViewItem extends Disposable implements INotificationVie } get canCollapse(): boolean { - return !this.hasPrompt(); + return !this.hasPrompt; } get expanded(): boolean { @@ -511,7 +526,7 @@ export class NotificationViewItem extends Disposable implements INotificationVie return true; // explicitly sticky } - const hasPrompt = this.hasPrompt(); + const hasPrompt = this.hasPrompt; if ( (hasPrompt && this._severity === Severity.Error) || // notification errors with actions are sticky (!hasPrompt && this._expanded) || // notifications that got expanded are sticky @@ -527,7 +542,7 @@ export class NotificationViewItem extends Disposable implements INotificationVie return !!this._silent; } - hasPrompt(): boolean { + private get hasPrompt(): boolean { if (!this._actions) { return false; } @@ -539,7 +554,7 @@ export class NotificationViewItem extends Disposable implements INotificationVie return this._actions.primary.length > 0; } - hasProgress(): boolean { + get hasProgress(): boolean { return !!this._progress; } @@ -621,7 +636,7 @@ export class NotificationViewItem extends Disposable implements INotificationVie } equals(other: INotificationViewItem): boolean { - if (this.hasProgress() || other.hasProgress()) { + if (this.hasProgress || other.hasProgress) { return false; } diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 0cae0da27f6..d1b30384874 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -53,6 +53,7 @@ export interface IViewContainerDescriptor { readonly extensionId?: ExtensionIdentifier; + readonly rejectAddedViews?: boolean; } export interface IViewContainersRegistry { @@ -214,6 +215,11 @@ export interface IViewDescriptorCollection extends IDisposable { export interface IViewContentDescriptor { readonly content: string; readonly when?: ContextKeyExpr | 'default'; + + /** + * ordered preconditions for each button in the content + */ + readonly preconditions?: (ContextKeyExpr | undefined)[]; } export interface IViewsRegistry { diff --git a/src/vs/workbench/contrib/backup/test/electron-browser/backupRestorer.test.ts b/src/vs/workbench/contrib/backup/test/electron-browser/backupRestorer.test.ts index ee976c15a01..eab7952a824 100644 --- a/src/vs/workbench/contrib/backup/test/electron-browser/backupRestorer.test.ts +++ b/src/vs/workbench/contrib/backup/test/electron-browser/backupRestorer.test.ts @@ -12,12 +12,10 @@ import { URI } from 'vs/base/common/uri'; import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { DefaultEndOfLine } from 'vs/editor/common/model'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { hashPath } from 'vs/workbench/services/backup/node/backupFileService'; import { NativeBackupTracker } from 'vs/workbench/contrib/backup/electron-browser/backupTracker'; -import { TestTextFileService, workbenchInstantiationService } from 'vs/workbench/test/electron-browser/workbenchTestServices'; +import { workbenchInstantiationService } from 'vs/workbench/test/electron-browser/workbenchTestServices'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; -import { BackupRestorer } from 'vs/workbench/contrib/backup/common/backupRestorer'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -33,6 +31,8 @@ import { NodeTestBackupFileService } from 'vs/workbench/services/backup/test/ele import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { isEqual } from 'vs/base/common/resources'; +import { TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; +import { BackupRestorer } from 'vs/workbench/contrib/backup/common/backupRestorer'; const userdataDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backuprestorer'); const backupHome = path.join(userdataDir, 'Backups'); @@ -51,15 +51,8 @@ class TestBackupRestorer extends BackupRestorer { } } -class ServiceAccessor { - constructor( - @ITextFileService public textFileService: TestTextFileService - ) { - } -} - suite('BackupRestorer', () => { - let accessor: ServiceAccessor; + let accessor: TestServiceAccessor; let disposables: IDisposable[] = []; @@ -105,7 +98,7 @@ suite('BackupRestorer', () => { const editorService: EditorService = instantiationService.createInstance(EditorService); instantiationService.stub(IEditorService, editorService); - accessor = instantiationService.createInstance(ServiceAccessor); + accessor = instantiationService.createInstance(TestServiceAccessor); await part.whenRestored; diff --git a/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts b/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts index a98b63d16a9..8420b62c436 100644 --- a/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts +++ b/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts @@ -10,10 +10,8 @@ import * as path from 'vs/base/common/path'; import * as pfs from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { hashPath } from 'vs/workbench/services/backup/node/backupFileService'; import { NativeBackupTracker } from 'vs/workbench/contrib/backup/electron-browser/backupTracker'; -import { TestLifecycleService, TestFilesConfigurationService, TestContextService, TestFileService, TestFileDialogService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; @@ -32,16 +30,14 @@ import { toResource } from 'vs/base/test/common/utils'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { ILogService } from 'vs/platform/log/common/log'; -import { HotExitConfiguration, IFileService } from 'vs/platform/files/common/files'; +import { HotExitConfiguration } from 'vs/platform/files/common/files'; import { ShutdownReason, ILifecycleService, BeforeShutdownEvent } from 'vs/platform/lifecycle/common/lifecycle'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IFileDialogService, ConfirmResult, IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IWorkspaceContextService, Workspace } from 'vs/platform/workspace/common/workspace'; import { IElectronService } from 'vs/platform/electron/node/electron'; import { BackupTracker } from 'vs/workbench/contrib/backup/common/backupTracker'; -import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { TestTextFileService, TestElectronService, workbenchInstantiationService } from 'vs/workbench/test/electron-browser/workbenchTestServices'; +import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/electron-browser/workbenchTestServices'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; @@ -52,23 +48,6 @@ const workspacesJsonPath = path.join(backupHome, 'workspaces.json'); const workspaceResource = URI.file(platform.isWindows ? 'c:\\workspace' : '/workspace'); const workspaceBackupPath = path.join(backupHome, hashPath(workspaceResource)); -class ServiceAccessor { - constructor( - @ILifecycleService public lifecycleService: TestLifecycleService, - @ITextFileService public textFileService: TestTextFileService, - @IFilesConfigurationService public filesConfigurationService: TestFilesConfigurationService, - @IWorkspaceContextService public contextService: TestContextService, - @IModelService public modelService: ModelServiceImpl, - @IFileService public fileService: TestFileService, - @IElectronService public electronService: TestElectronService, - @IFileDialogService public fileDialogService: TestFileDialogService, - @IBackupFileService public backupFileService: NodeTestBackupFileService, - @IWorkingCopyService public workingCopyService: IWorkingCopyService, - @IEditorService public editorService: IEditorService - ) { - } -} - class TestBackupTracker extends NativeBackupTracker { constructor( @@ -102,12 +81,12 @@ class BeforeShutdownEventImpl implements BeforeShutdownEvent { } suite('BackupTracker', () => { - let accessor: ServiceAccessor; + let accessor: TestServiceAccessor; let disposables: IDisposable[] = []; setup(async () => { const instantiationService = workbenchInstantiationService(); - accessor = instantiationService.createInstance(ServiceAccessor); + accessor = instantiationService.createInstance(TestServiceAccessor); disposables.push(Registry.as(EditorExtensions.Editors).registerEditor( EditorDescriptor.create( @@ -134,7 +113,7 @@ suite('BackupTracker', () => { return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE); }); - async function createTracker(): Promise<[ServiceAccessor, EditorPart, BackupTracker, IInstantiationService]> { + async function createTracker(): Promise<[TestServiceAccessor, EditorPart, BackupTracker, IInstantiationService]> { const backupFileService = new NodeTestBackupFileService(workspaceBackupPath); const instantiationService = workbenchInstantiationService(); instantiationService.stub(IBackupFileService, backupFileService); @@ -148,7 +127,7 @@ suite('BackupTracker', () => { const editorService: EditorService = instantiationService.createInstance(EditorService); instantiationService.stub(IEditorService, editorService); - accessor = instantiationService.createInstance(ServiceAccessor); + accessor = instantiationService.createInstance(TestServiceAccessor); await part.whenRestored; diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPane.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPane.ts index 0970caecdbb..808c009ed03 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPane.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPane.ts @@ -38,6 +38,7 @@ import type { IAsyncDataTreeViewState } from 'vs/base/browser/ui/tree/asyncDataT import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; const enum State { Data = 'data', @@ -86,10 +87,11 @@ export class BulkEditPane extends ViewPane { @IConfigurationService configurationService: IConfigurationService, @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, + @ITelemetryService telemetryService: ITelemetryService, ) { super( { ...options, titleMenuId: MenuId.BulkEditTitle }, - keybindingService, contextMenuService, configurationService, _contextKeyService, viewDescriptorService, _instaService, openerService, themeService + keybindingService, contextMenuService, configurationService, _contextKeyService, viewDescriptorService, _instaService, openerService, themeService, telemetryService ); this.element.classList.add('bulk-edit-panel', 'show-file-icons'); diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditTree.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditTree.ts index 6804221716a..0c55961052c 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditTree.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditTree.ts @@ -26,6 +26,7 @@ import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { WorkspaceFileEdit } from 'vs/editor/common/modes'; import { compare } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; +import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; // --- VIEW MODEL @@ -174,7 +175,10 @@ export class BulkEditDataSource implements IAsyncDataSource(provider => { + progress.report({ + message: localize( + 'formatting', + "Running '{0}' Formatter ([configure](command:workbench.action.openSettings?%5B%22editor.formatOnSave%22%5D)).", + provider.displayName || provider.extensionId && provider.extensionId.value || '???' + ) + }); + }); const editorOrModel = findEditor(model, this.codeEditorService) || model; - await this.instantiationService.invokeFunction(formatDocumentWithSelectedProvider, editorOrModel, FormattingMode.Silent, token); + await this.instantiationService.invokeFunction(formatDocumentWithSelectedProvider, editorOrModel, FormattingMode.Silent, nestedProgress, token); } } @@ -262,14 +270,37 @@ class CodeActionOnSaveParticipant implements ITextFileSaveParticipant { .map(x => new CodeActionKind(x)); progress.report({ message: localize('codeaction', "Quick Fixes") }); - await this.applyOnSaveActions(model, codeActionsOnSave, excludedActions, token); + await this.applyOnSaveActions(model, codeActionsOnSave, excludedActions, progress, token); } - private async applyOnSaveActions(model: ITextModel, codeActionsOnSave: readonly CodeActionKind[], excludes: readonly CodeActionKind[], token: CancellationToken): Promise { + private async applyOnSaveActions(model: ITextModel, codeActionsOnSave: readonly CodeActionKind[], excludes: readonly CodeActionKind[], progress: IProgress, token: CancellationToken): Promise { + + const getActionProgress = new class implements IProgress { + private _names: string[] = []; + private _report(): void { + progress.report({ + message: localize( + 'codeaction.get', + "Getting code actions from '{0}' ([configure](command:workbench.action.openSettings?%5B%22editor.codeActionsOnSave%22%5D)).", + this._names.map(name => `'${name}'`).join(', ') + ) + }); + } + report(provider: CodeActionProvider) { + if (provider.displayName) { + this._names.push(provider.displayName); + this._report(); + } + } + }; + for (const codeActionKind of codeActionsOnSave) { - const actionsToRun = await this.getActionsToRun(model, codeActionKind, excludes, token); + const actionsToRun = await this.getActionsToRun(model, codeActionKind, excludes, getActionProgress, token); try { - await this.applyCodeActions(actionsToRun.validActions); + for (const action of actionsToRun.validActions) { + progress.report({ message: localize('codeAction.apply', "Applying code action '{0}'.", action.title) }); + await this.instantiationService.invokeFunction(applyCodeAction, action); + } } catch { // Failure to apply a code action should not block other on save actions } finally { @@ -278,17 +309,11 @@ class CodeActionOnSaveParticipant implements ITextFileSaveParticipant { } } - private async applyCodeActions(actionsToRun: readonly CodeAction[]) { - for (const action of actionsToRun) { - await this.instantiationService.invokeFunction(applyCodeAction, action); - } - } - - private getActionsToRun(model: ITextModel, codeActionKind: CodeActionKind, excludes: readonly CodeActionKind[], token: CancellationToken) { + private getActionsToRun(model: ITextModel, codeActionKind: CodeActionKind, excludes: readonly CodeActionKind[], progress: IProgress, token: CancellationToken) { return getCodeActions(model, model.getFullModelRange(), { type: CodeActionTriggerType.Auto, filter: { include: codeActionKind, excludes: excludes, includeSourceActions: true }, - }, token); + }, progress, token); } } diff --git a/src/vs/workbench/contrib/codeEditor/browser/toggleColumnSelection.ts b/src/vs/workbench/contrib/codeEditor/browser/toggleColumnSelection.ts new file mode 100644 index 00000000000..fac092022e2 --- /dev/null +++ b/src/vs/workbench/contrib/codeEditor/browser/toggleColumnSelection.ts @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { Action } from 'vs/base/common/actions'; +import { MenuId, MenuRegistry, SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { CoreNavigationCommands } from 'vs/editor/browser/controller/coreCommands'; +import { Position } from 'vs/editor/common/core/position'; +import { Selection } from 'vs/editor/common/core/selection'; +import { CursorColumns } from 'vs/editor/common/controller/cursorCommon'; + +export class ToggleColumnSelectionAction extends Action { + public static readonly ID = 'editor.action.toggleColumnSelection'; + public static readonly LABEL = nls.localize('toggleColumnSelection', "Toggle Column Selection Mode"); + + constructor( + id: string, + label: string, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService + ) { + super(id, label); + } + + private _getCodeEditor(): ICodeEditor | null { + const codeEditor = this._codeEditorService.getFocusedCodeEditor(); + if (codeEditor) { + return codeEditor; + } + return this._codeEditorService.getActiveCodeEditor(); + } + + public async run(): Promise { + const oldValue = this._configurationService.getValue('editor.columnSelection'); + const codeEditor = this._getCodeEditor(); + await this._configurationService.updateValue('editor.columnSelection', !oldValue, ConfigurationTarget.USER); + const newValue = this._configurationService.getValue('editor.columnSelection'); + if (!codeEditor || codeEditor !== this._getCodeEditor() || oldValue === newValue || !codeEditor.hasModel()) { + return; + } + const cursors = codeEditor._getCursors(); + if (codeEditor.getOption(EditorOption.columnSelection)) { + const selection = codeEditor.getSelection(); + const modelSelectionStart = new Position(selection.selectionStartLineNumber, selection.selectionStartColumn); + const viewSelectionStart = cursors.context.convertModelPositionToViewPosition(modelSelectionStart); + const modelPosition = new Position(selection.positionLineNumber, selection.positionColumn); + const viewPosition = cursors.context.convertModelPositionToViewPosition(modelPosition); + + CoreNavigationCommands.MoveTo.runCoreEditorCommand(cursors, { + position: modelSelectionStart, + viewPosition: viewSelectionStart + }); + const visibleColumn = CursorColumns.visibleColumnFromColumn2(cursors.context.config, cursors.context.viewModel, viewPosition); + CoreNavigationCommands.ColumnSelect.runCoreEditorCommand(cursors, { + position: modelPosition, + viewPosition: viewPosition, + doColumnSelect: true, + mouseColumn: visibleColumn + 1 + }); + } else { + const columnSelectData = cursors.getColumnSelectData(); + const fromViewColumn = CursorColumns.columnFromVisibleColumn2(cursors.context.config, cursors.context.viewModel, columnSelectData.fromViewLineNumber, columnSelectData.fromViewVisualColumn); + const fromPosition = cursors.context.convertViewPositionToModelPosition(columnSelectData.fromViewLineNumber, fromViewColumn); + const toViewColumn = CursorColumns.columnFromVisibleColumn2(cursors.context.config, cursors.context.viewModel, columnSelectData.toViewLineNumber, columnSelectData.toViewVisualColumn); + const toPosition = cursors.context.convertViewPositionToModelPosition(columnSelectData.toViewLineNumber, toViewColumn); + + codeEditor.setSelection(new Selection(fromPosition.lineNumber, fromPosition.column, toPosition.lineNumber, toPosition.column)); + } + } +} + +const registry = Registry.as(ActionExtensions.WorkbenchActions); +registry.registerWorkbenchAction(SyncActionDescriptor.create(ToggleColumnSelectionAction, ToggleColumnSelectionAction.ID, ToggleColumnSelectionAction.LABEL), 'View: Toggle Column Selection Mode', nls.localize('view', "View")); + +MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, { + group: '4_config', + command: { + id: ToggleColumnSelectionAction.ID, + title: nls.localize({ key: 'miColumnSelection', comment: ['&& denotes a mnemonic'] }, "Column &&Selection Mode"), + toggled: ContextKeyExpr.equals('config.editor.columnSelection', true) + }, + order: 2 +}); diff --git a/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts b/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts index 05df06aa388..bc210c5a355 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts @@ -70,7 +70,7 @@ Registry.as(WorkbenchExtensions.Workbench).regi const registry = Registry.as(Extensions.WorkbenchActions); registry.registerWorkbenchAction(SyncActionDescriptor.create(ToggleMultiCursorModifierAction, ToggleMultiCursorModifierAction.ID, ToggleMultiCursorModifierAction.LABEL), 'Toggle Multi-Cursor Modifier'); MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, { - group: '3_multi', + group: '4_config', command: { id: ToggleMultiCursorModifierAction.ID, title: nls.localize('miMultiCursorAlt', "Switch to Alt+Click for Multi-Cursor") @@ -79,7 +79,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, { order: 1 }); MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, { - group: '3_multi', + group: '4_config', command: { id: ToggleMultiCursorModifierAction.ID, title: ( diff --git a/src/vs/workbench/contrib/codeEditor/electron-browser/selectionClipboard.ts b/src/vs/workbench/contrib/codeEditor/electron-browser/selectionClipboard.ts index 536d0f75c5c..592605c0759 100644 --- a/src/vs/workbench/contrib/codeEditor/electron-browser/selectionClipboard.ts +++ b/src/vs/workbench/contrib/codeEditor/electron-browser/selectionClipboard.ts @@ -20,10 +20,7 @@ import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; export class SelectionClipboard extends Disposable implements IEditorContribution { private static readonly SELECTION_LENGTH_LIMIT = 65536; @@ -119,15 +116,7 @@ class PasteSelectionClipboardAction extends EditorAction { id: 'editor.action.selectionClipboardPaste', label: nls.localize('actions.pasteSelectionClipboard', "Paste Selection Clipboard"), alias: 'Paste Selection Clipboard', - precondition: EditorContextKeys.writable, - kbOpts: { - kbExpr: ContextKeyExpr.and( - EditorContextKeys.editorTextFocus, - ContextKeyExpr.has('config.editor.selectionClipboard') - ), - primary: KeyMod.Shift | KeyCode.Insert, - weight: KeybindingWeight.EditorContrib - } + precondition: EditorContextKeys.writable }); } diff --git a/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts b/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts index c695bd3eaef..5c7917b7350 100644 --- a/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts +++ b/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts @@ -7,29 +7,23 @@ import * as assert from 'assert'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { FinalNewLineParticipant, TrimFinalNewLinesParticipant } from 'vs/workbench/contrib/codeEditor/browser/saveParticipants'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { workbenchInstantiationService, TestTextFileService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; import { toResource } from 'vs/base/test/common/utils'; -import { IModelService } from 'vs/editor/common/services/modelService'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; -import { ITextFileService, IResolvedTextFileEditorModel, snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; +import { IResolvedTextFileEditorModel, snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; import { SaveReason } from 'vs/workbench/common/editor'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; -class ServiceAccessor { - constructor(@ITextFileService public textFileService: TestTextFileService, @IModelService public modelService: IModelService) { - } -} - suite('MainThreadSaveParticipant', function () { let instantiationService: IInstantiationService; - let accessor: ServiceAccessor; + let accessor: TestServiceAccessor; setup(() => { instantiationService = workbenchInstantiationService(); - accessor = instantiationService.createInstance(ServiceAccessor); + accessor = instantiationService.createInstance(TestServiceAccessor); }); teardown(() => { diff --git a/src/vs/workbench/contrib/comments/browser/commentsView.ts b/src/vs/workbench/contrib/comments/browser/commentsView.ts index bd76b690c66..700a75d9d5d 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsView.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsView.ts @@ -27,6 +27,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; export class CommentsPanel extends ViewPane { @@ -51,9 +52,10 @@ export class CommentsPanel extends ViewPane { @IKeybindingService keybindingService: IKeybindingService, @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, - @ICommentService private readonly commentService: ICommentService + @ICommentService private readonly commentService: ICommentService, + @ITelemetryService telemetryService: ITelemetryService, ) { - super({ ...(options as IViewPaneOptions), id: COMMENTS_VIEW_ID, ariaHeaderLabel: COMMENTS_VIEW_TITLE }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); + super({ ...(options as IViewPaneOptions), id: COMMENTS_VIEW_ID, ariaHeaderLabel: COMMENTS_VIEW_TITLE }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); } public renderBody(container: HTMLElement): void { diff --git a/src/vs/workbench/contrib/customEditor/browser/commands.ts b/src/vs/workbench/contrib/customEditor/browser/commands.ts index 2d2f7825ecc..a6b3efb63be 100644 --- a/src/vs/workbench/contrib/customEditor/browser/commands.ts +++ b/src/vs/workbench/contrib/customEditor/browser/commands.ts @@ -15,9 +15,9 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { EditorViewColumn, viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor'; import { IEditorCommandsContext } from 'vs/workbench/common/editor'; -import { CustomFileEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput'; +import { CustomEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput'; import { defaultEditorId } from 'vs/workbench/contrib/customEditor/browser/customEditors'; -import { CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, CONTEXT_HAS_CUSTOM_EDITORS, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; +import { CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, CONTEXT_CUSTOM_EDITORS, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import type { ITextEditorOptions } from 'vs/platform/editor/common/editor'; @@ -80,7 +80,7 @@ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { title: REOPEN_WITH_TITLE, category: viewCategory, }, - when: CONTEXT_HAS_CUSTOM_EDITORS, + when: CONTEXT_CUSTOM_EDITORS.notEqualsTo(''), }); MenuRegistry.appendMenuItem(MenuId.EditorTitle, { @@ -89,9 +89,9 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { title: REOPEN_WITH_TITLE, category: viewCategory, }, - group: '3_open', + group: '6_reopen', order: 20, - when: CONTEXT_HAS_CUSTOM_EDITORS, + when: CONTEXT_CUSTOM_EDITORS.notEqualsTo(''), }); // #endregion @@ -114,19 +114,11 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { } public runCommand(accessor: ServicesAccessor): void { - const customEditorService = accessor.get(ICustomEditorService); - - const activeCustomEditor = customEditorService.activeCustomEditor; - if (!activeCustomEditor) { - return; + const editorService = accessor.get(IEditorService); + const activeInput = editorService.activeControl?.input; + if (activeInput instanceof CustomEditorInput) { + activeInput.undo(); } - - const model = customEditorService.models.get(activeCustomEditor.resource, activeCustomEditor.viewType); - if (!model) { - return; - } - - model.undo(); } }).register(); @@ -149,19 +141,11 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { } public runCommand(accessor: ServicesAccessor): void { - const customEditorService = accessor.get(ICustomEditorService); - - const activeCustomEditor = customEditorService.activeCustomEditor; - if (!activeCustomEditor) { - return; + const editorService = accessor.get(IEditorService); + const activeInput = editorService.activeControl?.input; + if (activeInput instanceof CustomEditorInput) { + activeInput.redo(); } - - const model = customEditorService.models.get(activeCustomEditor.resource, activeCustomEditor.viewType); - if (!model) { - return; - } - - model.redo(); } }).register(); @@ -171,7 +155,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { constructor() { super({ id: ToggleCustomEditorCommand.ID, - precondition: CONTEXT_HAS_CUSTOM_EDITORS, + precondition: CONTEXT_CUSTOM_EDITORS, }); } @@ -193,7 +177,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { const customEditorService = accessor.get(ICustomEditorService); let toggleView = defaultEditorId; - if (!(activeEditor instanceof CustomFileEditorInput)) { + if (!(activeEditor instanceof CustomEditorInput)) { const bestAvailableEditor = customEditorService.getContributedCustomEditors(targetResource).bestAvailableEditor; if (bestAvailableEditor) { toggleView = bestAvailableEditor.id; diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index 43e67a608d3..82ee3c38afa 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -16,20 +16,26 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ILabelService } from 'vs/platform/label/common/label'; import { GroupIdentifier, IEditorInput, IRevertOptions, ISaveOptions, Verbosity } from 'vs/workbench/common/editor'; import { ICustomEditorModel, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; -import { WebviewEditorOverlay, IWebviewService } from 'vs/workbench/contrib/webview/browser/webview'; +import { IWebviewService, WebviewEditorOverlay } from 'vs/workbench/contrib/webview/browser/webview'; import { IWebviewWorkbenchService, LazilyResolvedWebviewEditorInput } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; -import { AutoSaveMode, IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { AutoSaveMode, IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; + +export const enum ModelType { + Custom = 'custom', + Text = 'text', +} + +export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { -export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { public static typeId = 'workbench.editors.webviewEditor'; private readonly _editorResource: URI; get resource() { return this._editorResource; } - private _model?: ICustomEditorModel; - + private _model?: { readonly type: ModelType.Custom, readonly model: ICustomEditorModel } | { readonly type: ModelType.Text }; constructor( resource: URI, @@ -43,14 +49,18 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { @ICustomEditorService private readonly customEditorService: ICustomEditorService, @IFileDialogService private readonly fileDialogService: IFileDialogService, @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, - @IEditorService private readonly editorService: IEditorService + @IEditorService private readonly editorService: IEditorService, + @ITextFileService private readonly textFileService: ITextFileService, + ) { super(id, viewType, '', webview, webviewService, webviewWorkbenchService); this._editorResource = resource; } + public modelType?: ModelType; + public getTypeId(): string { - return CustomFileEditorInput.typeId; + return CustomEditorInput.typeId; } public supportsSplitEditor() { @@ -62,13 +72,8 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { return basename(this.labelService.getUriLabel(this.resource)); } - @memoize - getDescription(): string | undefined { - return super.getDescription(); - } - matches(other: IEditorInput): boolean { - return this === other || (other instanceof CustomFileEditorInput + return this === other || (other instanceof CustomEditorInput && this.viewType === other.viewType && isEqual(this.resource, other.resource)); } @@ -101,11 +106,24 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { } public isReadonly(): boolean { - return false; + return false; // TODO } public isDirty(): boolean { - return this._model ? this._model.isDirty() : false; + if (!this._model) { + return false; + } + + switch (this._model.type) { + case ModelType.Text: + return this.textFileService.isDirty(this.resource); + + case ModelType.Custom: + return this._model.model.isDirty(); + + default: + throw new Error('Unknown model type'); + } } public isSaving(): boolean { @@ -125,12 +143,20 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { return undefined; } - const result = await this._model.save(options); - if (!result) { - return undefined; + switch (this._model.type) { + case ModelType.Text: + { + const result = await this.textFileService.save(this.resource, options); + return result ? this : undefined; + } + case ModelType.Custom: + { + const result = await this._model.model.save(options); + return result ? this : undefined; + } + default: + throw new Error('Unknown model type'); } - - return this; } public async saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise { @@ -144,31 +170,81 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { return undefined; // save cancelled } - if (!await this._model.saveAs(this._editorResource, target, options)) { - return undefined; + switch (this._model.type) { + case ModelType.Text: + if (!await this.textFileService.saveAs(this.resource, target, options)) { + return undefined; + } + break; + + case ModelType.Custom: + if (!await this._model.model.saveAs(this._editorResource, target, options)) { + return undefined; + } + break; + + default: + throw new Error('Unknown model type'); } return this.handleMove(groupId, target) || this.editorService.createInput({ resource: target, forceFile: true }); } - public revert(group: GroupIdentifier, options?: IRevertOptions): Promise { - return this._model ? this._model.revert(options) : Promise.resolve(false); + public async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { + if (!this._model) { + return false; + } + + switch (this._model.type) { + case ModelType.Text: + return this.textFileService.revert(this.resource, options); + + case ModelType.Custom: + return this._model.model.revert(options); + + default: + throw new Error('Unknown model type'); + } } public async resolve(): Promise { - this._model = await this.customEditorService.models.resolve(this.resource, this.viewType); - this._register(this._model.onDidChangeDirty(() => this._onDidChangeDirty.fire())); + const editorModel = await super.resolve(); + if (!this._model) { + switch (this.modelType) { + case ModelType.Custom: + const model = await this.customEditorService.models.resolve(this.resource, this.viewType); + this._model = { type: ModelType.Custom, model }; + this._register(model.onDidChangeDirty(() => this._onDidChangeDirty.fire())); + + break; + + case ModelType.Text: + this._model = { type: ModelType.Text, }; + this.textFileService.files.onDidChangeDirty(e => { + if (isEqual(this.resource, e.resource)) { + this._onDidChangeDirty.fire(); + } + }); + + break; + + default: + throw new Error('Unknown model type'); + } + } + if (this.isDirty()) { this._onDidChangeDirty.fire(); } - return await super.resolve(); + + return editorModel; } public handleMove(groupId: GroupIdentifier, uri: URI, options?: ITextEditorOptions): IEditorInput | undefined { const editorInfo = this.customEditorService.getCustomEditor(this.viewType); if (editorInfo?.matches(uri)) { const webview = assertIsDefined(this.takeOwnershipOfWebview()); - const newInput = this.instantiationService.createInstance(CustomFileEditorInput, + const newInput = this.instantiationService.createInstance(CustomEditorInput, uri, this.viewType, generateUuid(), @@ -178,4 +254,42 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { } return undefined; } + + public undo(): void { + if (!this._model) { + return; + } + + switch (this._model.type) { + case ModelType.Custom: + this._model.model.undo(); + return; + + case ModelType.Text: + this.textFileService.files.get(this.resource)?.textEditorModel?.undo(); + return; + + default: + throw new Error('Unknown model type'); + } + } + + public redo(): void { + if (!this._model) { + return; + } + + switch (this._model.type) { + case ModelType.Custom: + this._model.model.redo(); + return; + + case ModelType.Text: + this.textFileService.files.get(this.resource)?.textEditorModel?.redo(); + return; + + default: + throw new Error('Unknown model type'); + } + } } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts index a19bf46c58f..558dc3d380d 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts @@ -6,14 +6,14 @@ import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { CustomFileEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput'; +import { CustomEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput'; import { WebviewEditorInputFactory } from 'vs/workbench/contrib/webview/browser/webviewEditorInputFactory'; import { IWebviewWorkbenchService } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; import { Lazy } from 'vs/base/common/lazy'; export class CustomEditorInputFactory extends WebviewEditorInputFactory { - public static readonly ID = CustomFileEditorInput.typeId; + public static readonly ID = CustomEditorInput.typeId; public constructor( @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -22,10 +22,11 @@ export class CustomEditorInputFactory extends WebviewEditorInputFactory { super(webviewWorkbenchService); } - public serialize(input: CustomFileEditorInput): string | undefined { + public serialize(input: CustomEditorInput): string | undefined { const data = { ...this.toJson(input), - editorResource: input.resource.toJSON() + editorResource: input.resource.toJSON(), + modelType: input.modelType }; try { @@ -38,7 +39,7 @@ export class CustomEditorInputFactory extends WebviewEditorInputFactory { public deserialize( _instantiationService: IInstantiationService, serializedEditorInput: string - ): CustomFileEditorInput { + ): CustomEditorInput { const data = this.fromJson(serializedEditorInput); const id = data.id || generateUuid(); @@ -50,10 +51,13 @@ export class CustomEditorInputFactory extends WebviewEditorInputFactory { return webviewInput.webview; }); - const customInput = this._instantiationService.createInstance(CustomFileEditorInput, URI.from((data as any).editorResource), data.viewType, id, webview); + const customInput = this._instantiationService.createInstance(CustomEditorInput, URI.from((data as any).editorResource), data.viewType, id, webview); if (typeof data.group === 'number') { customInput.updateGroup(data.group); } + if ((data as any).modelType) { + customInput.modelType = (data as any).modelType; + } return customInput; } } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts index f9e9490b1b5..ca4581ede56 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts @@ -7,7 +7,7 @@ import { coalesce } from 'vs/base/common/arrays'; import { Emitter } from 'vs/base/common/event'; import { Lazy } from 'vs/base/common/lazy'; import { Disposable } from 'vs/base/common/lifecycle'; -import { basename, isEqual, extname } from 'vs/base/common/resources'; +import { basename, extname, isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import * as nls from 'vs/nls'; @@ -25,13 +25,13 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { EditorInput, EditorOptions, IEditor, IEditorInput } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { webviewEditorsExtensionPoint } from 'vs/workbench/contrib/customEditor/browser/extensionPoint'; -import { CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, CONTEXT_HAS_CUSTOM_EDITORS, CustomEditorInfo, CustomEditorInfoCollection, CustomEditorPriority, CustomEditorSelector, ICustomEditor, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; +import { CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, CONTEXT_CUSTOM_EDITORS, CustomEditorInfo, CustomEditorInfoCollection, CustomEditorPriority, CustomEditorSelector, ICustomEditor, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { CustomEditorModelManager } from 'vs/workbench/contrib/customEditor/common/customEditorModelManager'; import { IWebviewService, webviewHasOwnEditFunctionsContext } from 'vs/workbench/contrib/webview/browser/webview'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService, IOpenEditorOverride } from 'vs/workbench/services/editor/common/editorService'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { CustomFileEditorInput } from './customEditorInput'; +import { CustomEditorInput } from './customEditorInput'; export const defaultEditorId = 'default'; @@ -98,7 +98,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ private readonly _models: CustomEditorModelManager; - private readonly _hasCustomEditor: IContextKey; + private readonly _customEditorContextKey: IContextKey; private readonly _focusedCustomEditorIsEditable: IContextKey; private readonly _webviewHasOwnEditFunctions: IContextKey; @@ -118,7 +118,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ this._models = new CustomEditorModelManager(workingCopyService, labelService); - this._hasCustomEditor = CONTEXT_HAS_CUSTOM_EDITORS.bindTo(contextKeyService); + this._customEditorContextKey = CONTEXT_CUSTOM_EDITORS.bindTo(contextKeyService); this._focusedCustomEditorIsEditable = CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE.bindTo(contextKeyService); this._webviewHasOwnEditFunctions = webviewHasOwnEditFunctionsContext.bindTo(contextKeyService); @@ -138,7 +138,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ public get activeCustomEditor(): ICustomEditor | undefined { const activeInput = this.editorService.activeControl?.input; - if (!(activeInput instanceof CustomFileEditorInput)) { + if (!(activeInput instanceof CustomEditorInput)) { return undefined; } const resource = activeInput.resource; @@ -175,7 +175,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ let currentlyOpenedEditorType: undefined | string; for (const editor of group ? group.editors : []) { if (editor.resource && isEqual(editor.resource, resource)) { - currentlyOpenedEditorType = editor instanceof CustomFileEditorInput ? editor.viewType : defaultEditorId; + currentlyOpenedEditorType = editor instanceof CustomEditorInput ? editor.viewType : defaultEditorId; break; } } @@ -271,7 +271,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ const webview = new Lazy(() => { return this.webviewService.createWebviewEditorOverlay(id, { customClasses: options?.customClasses }, {}); }); - const input = this.instantiationService.createInstance(CustomFileEditorInput, resource, viewType, id, webview); + const input = this.instantiationService.createInstance(CustomEditorInput, resource, viewType, id, webview); if (group) { input.updateGroup(group.id); } @@ -297,7 +297,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ options: options ? EditorOptions.create(options) : undefined, }], targetGroup); - if (existing instanceof CustomFileEditorInput) { + if (existing instanceof CustomEditorInput) { existing.dispose(); } } @@ -310,7 +310,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ const activeControl = this.editorService.activeControl; const resource = activeControl?.input.resource; if (!resource) { - this._hasCustomEditor.reset(); + this._customEditorContextKey.reset(); this._focusedCustomEditorIsEditable.reset(); this._webviewHasOwnEditFunctions.reset(); return; @@ -320,24 +320,20 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ ...this.getContributedCustomEditors(resource).allEditors, ...this.getUserConfiguredCustomEditors(resource).allEditors, ]; - this._hasCustomEditor.set(possibleEditors.length > 0); - this._focusedCustomEditorIsEditable.set(activeControl?.input instanceof CustomFileEditorInput); + this._customEditorContextKey.set(possibleEditors.map(x => x.id).join(',')); + this._focusedCustomEditorIsEditable.set(activeControl?.input instanceof CustomEditorInput); this._webviewHasOwnEditFunctions.set(possibleEditors.length > 0); } private handleMovedFileInOpenedFileEditors(oldResource: URI, newResource: URI): void { for (const group of this.editorGroupService.groups) { for (const editor of group.editors) { - if (!(editor instanceof CustomFileEditorInput)) { + if (!(editor instanceof CustomEditorInput)) { continue; } const editorInfo = this._editorInfoStore.get(editor.viewType); - if (!editorInfo) { - continue; - } - - if (!editorInfo.matches(newResource)) { + if (!editorInfo?.matches(newResource)) { continue; } @@ -371,7 +367,7 @@ export class CustomEditorContribution extends Disposable implements IWorkbenchCo })); this._register(this.editorService.onDidCloseEditor(({ editor }) => { - if (!(editor instanceof CustomFileEditorInput)) { + if (!(editor instanceof CustomEditorInput)) { return; } @@ -386,7 +382,7 @@ export class CustomEditorContribution extends Disposable implements IWorkbenchCo options: ITextEditorOptions | undefined, group: IEditorGroup ): IOpenEditorOverride | undefined { - if (editor instanceof CustomFileEditorInput) { + if (editor instanceof CustomEditorInput) { if (editor.group === group.id) { // No need to do anything return undefined; @@ -483,7 +479,7 @@ export class CustomEditorContribution extends Disposable implements IWorkbenchCo group: IEditorGroup ): IOpenEditorOverride | undefined { const getCustomEditorOverrideForSubInput = (subInput: IEditorInput, customClasses: string): EditorInput | undefined => { - if (subInput instanceof CustomFileEditorInput) { + if (subInput instanceof CustomEditorInput) { return undefined; } const resource = subInput.resource; diff --git a/src/vs/workbench/contrib/customEditor/browser/extensionPoint.ts b/src/vs/workbench/contrib/customEditor/browser/extensionPoint.ts index 2a126dabefe..7c7a65b2d63 100644 --- a/src/vs/workbench/contrib/customEditor/browser/extensionPoint.ts +++ b/src/vs/workbench/contrib/customEditor/browser/extensionPoint.ts @@ -24,7 +24,7 @@ interface IWebviewEditorsExtensionPoint { } const webviewEditorsContribution: IJSONSchema = { - description: nls.localize('contributes.webviewEditors', 'Contributes webview editors.'), + description: nls.localize('contributes.customEditors', 'Contributed custom editors.'), type: 'array', defaultSnippets: [{ body: [{ viewType: '', displayName: '' }] }], items: { @@ -76,7 +76,7 @@ const webviewEditorsContribution: IJSONSchema = { }; export const webviewEditorsExtensionPoint = ExtensionsRegistry.registerExtensionPoint({ - extensionPoint: 'webviewEditors', + extensionPoint: 'customEditors', deps: [languagesExtPoint], jsonSchema: webviewEditorsContribution }); diff --git a/src/vs/workbench/contrib/customEditor/browser/webviewEditor.contribution.ts b/src/vs/workbench/contrib/customEditor/browser/webviewEditor.contribution.ts index 663521b2042..00f28e3ecd9 100644 --- a/src/vs/workbench/contrib/customEditor/browser/webviewEditor.contribution.ts +++ b/src/vs/workbench/contrib/customEditor/browser/webviewEditor.contribution.ts @@ -17,7 +17,7 @@ import { CustomEditorInputFactory } from 'vs/workbench/contrib/customEditor/brow import { ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { WebviewEditor } from 'vs/workbench/contrib/webview/browser/webviewEditor'; import './commands'; -import { CustomFileEditorInput } from './customEditorInput'; +import { CustomEditorInput } from './customEditorInput'; import { CustomEditorContribution, customEditorsAssociationsKey, CustomEditorService } from './customEditors'; registerSingleton(ICustomEditorService, CustomEditorService); @@ -31,7 +31,7 @@ Registry.as(EditorExtensions.Editors).registerEditor( WebviewEditor.ID, 'Webview Editor', ), [ - new SyncDescriptor(CustomFileEditorInput) + new SyncDescriptor(CustomEditorInput) ]); Registry.as(EditorInputExtensions.EditorInputFactories).registerEditorInputFactory( diff --git a/src/vs/workbench/contrib/customEditor/common/customEditor.ts b/src/vs/workbench/contrib/customEditor/common/customEditor.ts index e7b613ef6fc..a0e84261e3a 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditor.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditor.ts @@ -18,7 +18,7 @@ import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCo export const ICustomEditorService = createDecorator('customEditorService'); -export const CONTEXT_HAS_CUSTOM_EDITORS = new RawContextKey('hasCustomEditors', false); +export const CONTEXT_CUSTOM_EDITORS = new RawContextKey('customEditors', ''); export const CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE = new RawContextKey('focusedCustomEditorIsEditable', false); export interface ICustomEditor { @@ -43,8 +43,6 @@ export interface ICustomEditorService { promptOpenWith(resource: URI, options?: ITextEditorOptions, group?: IEditorGroup): Promise; } -export type CustomEditorEdit = number; - export interface ICustomEditorModelManager { get(resource: URI, viewType: string): ICustomEditorModel | undefined; @@ -69,23 +67,22 @@ export interface CustomEditorSaveAsEvent { export interface ICustomEditorModel extends IWorkingCopy { readonly viewType: string; - readonly onUndo: Event<{ edits: readonly CustomEditorEdit[], trigger: any | undefined }>; - readonly onApplyEdit: Event<{ edits: readonly CustomEditorEdit[], trigger: any | undefined }>; - readonly onDisposeEdits: Event<{ edits: readonly CustomEditorEdit[] }>; + readonly onUndo: Event; + readonly onRedo: Event; + readonly onRevert: Event; readonly onWillSave: Event; readonly onWillSaveAs: Event; onBackup(f: () => CancelablePromise): void; + setDirty(dirty: boolean): void; undo(): void; redo(): void; revert(options?: IRevertOptions): Promise; save(options?: ISaveOptions): Promise; saveAs(resource: URI, targetResource: URI, currentOptions?: ISaveOptions): Promise; - - pushEdit(edit: CustomEditorEdit, trigger: any): void; } export const enum CustomEditorPriority { diff --git a/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts b/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts index 6448fbf2a63..6adac4c6199 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts @@ -8,7 +8,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; -import { CustomEditorEdit, CustomEditorSaveAsEvent, CustomEditorSaveEvent, ICustomEditorModel } from 'vs/workbench/contrib/customEditor/common/customEditor'; +import { CustomEditorSaveAsEvent, CustomEditorSaveEvent, ICustomEditorModel } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { ILabelService } from 'vs/platform/label/common/label'; import { basename } from 'vs/base/common/path'; @@ -38,10 +38,8 @@ namespace HotExitState { export class CustomEditorModel extends Disposable implements ICustomEditorModel { - private _currentEditIndex: number = -1; - private _savePoint: number = -1; - private readonly _edits: Array = []; private _hotExitState: HotExitState.State = HotExitState.NotSupported; + private _dirty = false; constructor( public readonly viewType: string, @@ -51,11 +49,6 @@ export class CustomEditorModel extends Disposable implements ICustomEditorModel super(); } - dispose() { - this._onDisposeEdits.fire({ edits: this._edits }); - super.dispose(); - } - //#region IWorkingCopy public get resource() { @@ -71,31 +64,31 @@ export class CustomEditorModel extends Disposable implements ICustomEditorModel } public isDirty(): boolean { - return this._edits.length > 0 && this._savePoint !== this._currentEditIndex; + return this._dirty; } - protected readonly _onDidChangeDirty: Emitter = this._register(new Emitter()); + private readonly _onDidChangeDirty: Emitter = this._register(new Emitter()); readonly onDidChangeDirty: Event = this._onDidChangeDirty.event; - protected readonly _onDidChangeContent: Emitter = this._register(new Emitter()); + private readonly _onDidChangeContent: Emitter = this._register(new Emitter()); readonly onDidChangeContent: Event = this._onDidChangeContent.event; //#endregion - protected readonly _onUndo = this._register(new Emitter<{ edits: readonly CustomEditorEdit[], trigger: any | undefined }>()); - readonly onUndo = this._onUndo.event; + private readonly _onUndo = this._register(new Emitter()); + public readonly onUndo = this._onUndo.event; - protected readonly _onApplyEdit = this._register(new Emitter<{ edits: readonly CustomEditorEdit[], trigger: any | undefined }>()); - readonly onApplyEdit = this._onApplyEdit.event; + private readonly _onRedo = this._register(new Emitter()); + public readonly onRedo = this._onRedo.event; - protected readonly _onDisposeEdits = this._register(new Emitter<{ edits: readonly CustomEditorEdit[] }>()); - readonly onDisposeEdits = this._onDisposeEdits.event; + private readonly _onRevert = this._register(new Emitter()); + public readonly onRevert = this._onRevert.event; - protected readonly _onWillSave = this._register(new Emitter()); - readonly onWillSave = this._onWillSave.event; + private readonly _onWillSave = this._register(new Emitter()); + public readonly onWillSave = this._onWillSave.event; - protected readonly _onWillSaveAs = this._register(new Emitter()); - readonly onWillSaveAs = this._onWillSaveAs.event; + private readonly _onWillSaveAs = this._register(new Emitter()); + public readonly onWillSaveAs = this._onWillSaveAs.event; private _onBackup: undefined | (() => CancelablePromise); @@ -110,38 +103,30 @@ export class CustomEditorModel extends Disposable implements ICustomEditorModel } } - public pushEdit(edit: CustomEditorEdit, trigger: any) { - this.spliceEdits(edit); + public setDirty(dirty: boolean): void { + this._onDidChangeContent.fire(); - this._currentEditIndex = this._edits.length - 1; - this.updateDirty(); - this._onApplyEdit.fire({ edits: [edit], trigger }); - this.updateContentChanged(); - } - - private spliceEdits(editToInsert?: CustomEditorEdit) { - const start = this._currentEditIndex + 1; - const toRemove = this._edits.length - this._currentEditIndex; - - const removedEdits = editToInsert - ? this._edits.splice(start, toRemove, editToInsert) - : this._edits.splice(start, toRemove); - - if (removedEdits.length) { - this._onDisposeEdits.fire({ edits: removedEdits }); + if (this._dirty !== dirty) { + this._dirty = dirty; + this._onDidChangeDirty.fire(); } } - private updateDirty() { - // TODO@matt this should to be more fine grained and avoid - // emitting events if there was no change actually - this._onDidChangeDirty.fire(); + public async revert(_options?: IRevertOptions) { + if (!this._dirty) { + return true; + } + + this._onRevert.fire(); + return true; } - private updateContentChanged() { - // TODO@matt revisit that this method is being called correctly - // on each case of content change within the custom editor - this._onDidChangeContent.fire(); + public undo() { + this._onUndo.fire(); + } + + public redo() { + this._onRedo.fire(); } public async save(_options?: ISaveOptions): Promise { @@ -158,8 +143,7 @@ export class CustomEditorModel extends Disposable implements ICustomEditorModel return false; } - this._savePoint = this._currentEditIndex; - this.updateDirty(); + this.setDirty(false); return true; } @@ -179,62 +163,11 @@ export class CustomEditorModel extends Disposable implements ICustomEditorModel return false; } - this._savePoint = this._currentEditIndex; - this.updateDirty(); + this.setDirty(false); return true; } - public async revert(_options?: IRevertOptions) { - if (this._currentEditIndex === this._savePoint) { - return true; - } - - if (this._currentEditIndex >= this._savePoint) { - const editsToUndo = this._edits.slice(this._savePoint, this._currentEditIndex); - this._onUndo.fire({ edits: editsToUndo.reverse(), trigger: undefined }); - } else if (this._currentEditIndex < this._savePoint) { - const editsToRedo = this._edits.slice(this._currentEditIndex, this._savePoint); - this._onApplyEdit.fire({ edits: editsToRedo, trigger: undefined }); - } - - this._currentEditIndex = this._savePoint; - this.spliceEdits(); - - this.updateDirty(); - this.updateContentChanged(); - return true; - } - - public undo() { - if (this._currentEditIndex < 0) { - // nothing to undo - return; - } - - const undoneEdit = this._edits[this._currentEditIndex]; - --this._currentEditIndex; - this._onUndo.fire({ edits: [undoneEdit], trigger: undefined }); - - this.updateDirty(); - this.updateContentChanged(); - } - - public redo() { - if (this._currentEditIndex >= this._edits.length - 1) { - // nothing to redo - return; - } - - ++this._currentEditIndex; - const redoneEdit = this._edits[this._currentEditIndex]; - - this._onApplyEdit.fire({ edits: [redoneEdit], trigger: undefined }); - - this.updateDirty(); - this.updateContentChanged(); - } - public async backup(): Promise { if (this._hotExitState === HotExitState.NotSupported) { throw new Error('Not supported'); diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index d64f9b7134e..da287aae45d 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -35,6 +35,7 @@ import { Gesture } from 'vs/base/browser/touch'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; const $ = dom.$; @@ -71,8 +72,9 @@ export class BreakpointsView extends ViewPane { @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IContextKeyService contextKeyService: IContextKeyService, @IOpenerService openerService: IOpenerService, + @ITelemetryService telemetryService: ITelemetryService, ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('breakpointsSection', "Breakpoints Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('breakpointsSection', "Breakpoints Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.minimumBodySize = this.maximumBodySize = getExpandedBodySize(this.debugService.getModel()); this._register(this.debugService.getModel().onDidChangeBreakpoints(() => this.onBreakpointsChange())); diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index 5ae2c952efe..377041b25f9 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -37,6 +37,7 @@ import { CollapseAction } from 'vs/workbench/browser/viewlet'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; const $ = dom.$; @@ -101,8 +102,9 @@ export class CallStackView extends ViewPane { @IContextKeyService readonly contextKeyService: IContextKeyService, @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, + @ITelemetryService telemetryService: ITelemetryService, ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('callstackSection', "Call Stack Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('callstackSection', "Call Stack Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.callStackItemType = CONTEXT_CALLSTACK_ITEM_TYPE.bindTo(contextKeyService); this.contributedContextMenu = menuService.createMenu(MenuId.DebugCallStackContext, contextKeyService); diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 30ae2db8bbf..d01fecdbb3c 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -267,7 +267,7 @@ configurationRegistry.registerConfiguration({ default: true }, 'debug.onTaskErrors': { - enum: ['debugAnyway', 'showErrors', 'prompt', 'cancel'], + enum: ['debugAnyway', 'showErrors', 'prompt', 'abort'], enumDescriptions: [nls.localize('debugAnyway', "Ignore task errors and start debugging."), nls.localize('showErrors', "Show the Problems view and do not start debugging."), nls.localize('prompt', "Prompt user."), nls.localize('cancel', "Cancel debugging.")], description: nls.localize('debug.onTaskErrors', "Controls what to do when errors are encountered after running a preLaunchTask."), default: 'prompt' diff --git a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts index 405b81daeed..1a4ec6cf4d7 100644 --- a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts @@ -11,7 +11,6 @@ import * as objects from 'vs/base/common/objects'; import { URI as uri } from 'vs/base/common/uri'; import * as resources from 'vs/base/common/resources'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; -import * as editorCommon from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; import { IEditor } from 'vs/workbench/common/editor'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; @@ -434,15 +433,8 @@ export class ConfigurationManager implements IConfigurationManager { return this.debuggers.filter(dbg => strings.equalsIgnoreCase(dbg.type, type)).pop(); } - getDebuggerLabelsForEditor(editor: editorCommon.IEditor | undefined): string[] { - if (isCodeEditor(editor)) { - const model = editor.getModel(); - const language = model ? model.getLanguageIdentifier().language : undefined; - - return this.debuggers.filter(a => language && a.languages && a.languages.indexOf(language) >= 0).map(d => d.label); - } - - return []; + isDebuggerInterestedInLanguage(language: string): boolean { + return this.debuggers.filter(a => language && a.languages && a.languages.indexOf(language) >= 0).length > 0; } async guessDebugger(type?: string): Promise { @@ -527,18 +519,17 @@ abstract class AbstractLaunch { if (!config || (!Array.isArray(config.configurations) && !Array.isArray(config.compounds))) { return []; } else { - const names: string[] = []; + const configurations: (IConfig | ICompound)[] = []; if (config.configurations) { - names.push(...config.configurations.filter(cfg => cfg && typeof cfg.name === 'string').map(cfg => cfg.name)); + configurations.push(...config.configurations.filter(cfg => cfg && typeof cfg.name === 'string')); } if (includeCompounds && config.compounds) { if (config.compounds) { - names.push(...config.compounds.filter(compound => typeof compound.name === 'string' && compound.configurations && compound.configurations.length) - .map(compound => compound.name)); + configurations.push(...config.compounds.filter(compound => typeof compound.name === 'string' && compound.configurations && compound.configurations.length)); } } - return names; + return getVisibleAndSorted(configurations).map(c => c.name); } } diff --git a/src/vs/workbench/contrib/debug/browser/debugHover.ts b/src/vs/workbench/contrib/debug/browser/debugHover.ts index 5de2c78eea2..0a9b8964e1a 100644 --- a/src/vs/workbench/contrib/debug/browser/debugHover.ts +++ b/src/vs/workbench/contrib/debug/browser/debugHover.ts @@ -209,7 +209,7 @@ export class DebugHoverWidget implements IContentWidget { if (!matchingExpression) { const lineContent = model.getLineContent(pos.lineNumber); - matchingExpression = lineContent.substring(rng.startColumn - 1, rng.endColumn); + matchingExpression = lineContent.substring(rng.startColumn - 1, rng.endColumn - 1); } } diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 5b9c4ac37dd..214d3763448 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -19,7 +19,7 @@ import { RawDebugSession } from 'vs/workbench/contrib/debug/browser/rawDebugSess import { IProductService } from 'vs/platform/product/common/productService'; import { IWorkspaceFolder, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { RunOnceScheduler } from 'vs/base/common/async'; +import { RunOnceScheduler, Queue } from 'vs/base/common/async'; import { generateUuid } from 'vs/base/common/uuid'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug'; @@ -807,59 +807,58 @@ export class DebugSession implements IDebugSession { this._onDidChangeState.fire(); })); - let outpuPromises: Promise[] = []; + const outputQueue = new Queue(); this.rawListeners.push(this.raw.onDidOutput(async event => { - if (!event.body || !this.raw) { - return; - } - - const outputSeverity = event.body.category === 'stderr' ? severity.Error : event.body.category === 'console' ? severity.Warning : severity.Info; - if (event.body.category === 'telemetry') { - // only log telemetry events from debug adapter if the debug extension provided the telemetry key - // and the user opted in telemetry - if (this.raw.customTelemetryService && this.telemetryService.isOptedIn) { - // __GDPR__TODO__ We're sending events in the name of the debug extension and we can not ensure that those are declared correctly. - this.raw.customTelemetryService.publicLog(event.body.output, event.body.data); + outputQueue.queue(async () => { + if (!event.body || !this.raw) { + return; } - return; - } + const outputSeverity = event.body.category === 'stderr' ? severity.Error : event.body.category === 'console' ? severity.Warning : severity.Info; + if (event.body.category === 'telemetry') { + // only log telemetry events from debug adapter if the debug extension provided the telemetry key + // and the user opted in telemetry + if (this.raw.customTelemetryService && this.telemetryService.isOptedIn) { + // __GDPR__TODO__ We're sending events in the name of the debug extension and we can not ensure that those are declared correctly. + this.raw.customTelemetryService.publicLog(event.body.output, event.body.data); + } - // Make sure to append output in the correct order by properly waiting on preivous promises #33822 - const waitFor = outpuPromises.slice(); - const source = event.body.source && event.body.line ? { - lineNumber: event.body.line, - column: event.body.column ? event.body.column : 1, - source: this.getSource(event.body.source) - } : undefined; + return; + } - if (event.body.group === 'start' || event.body.group === 'startCollapsed') { - const expanded = event.body.group === 'start'; - this.repl.startGroup(event.body.output || '', expanded, source); - return; - } - if (event.body.group === 'end') { - this.repl.endGroup(); - // Do not return, the end event can have additional output in it - } + // Make sure to append output in the correct order by properly waiting on preivous promises #33822 + const source = event.body.source && event.body.line ? { + lineNumber: event.body.line, + column: event.body.column ? event.body.column : 1, + source: this.getSource(event.body.source) + } : undefined; - if (event.body.variablesReference) { - const container = new ExpressionContainer(this, undefined, event.body.variablesReference, generateUuid()); - outpuPromises.push(container.getChildren().then(async children => { - await Promise.all(waitFor); - children.forEach(child => { - // Since we can not display multiple trees in a row, we are displaying these variables one after the other (ignoring their names) - (child).name = null; - this.appendToRepl(child, outputSeverity, source); + if (event.body.group === 'start' || event.body.group === 'startCollapsed') { + const expanded = event.body.group === 'start'; + this.repl.startGroup(event.body.output || '', expanded, source); + return; + } + if (event.body.group === 'end') { + this.repl.endGroup(); + if (!event.body.output) { + // Only return if the end event does not have additional output in it + return; + } + } + + if (event.body.variablesReference) { + const container = new ExpressionContainer(this, undefined, event.body.variablesReference, generateUuid()); + await container.getChildren().then(children => { + children.forEach(child => { + // Since we can not display multiple trees in a row, we are displaying these variables one after the other (ignoring their names) + (child).name = null; + this.appendToRepl(child, outputSeverity, source); + }); }); - })); - } else if (typeof event.body.output === 'string') { - await Promise.all(waitFor); - this.appendToRepl(event.body.output, outputSeverity, source); - } - - await Promise.all(outpuPromises); - outpuPromises = []; + } else if (typeof event.body.output === 'string') { + this.appendToRepl(event.body.output, outputSeverity, source); + } + }); })); this.rawListeners.push(this.raw.onDidBreakpoint(event => { diff --git a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts index ceb03abfcc9..af223d0a938 100644 --- a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts +++ b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts @@ -39,6 +39,7 @@ import type { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTr import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; const NEW_STYLE_COMPRESS = true; @@ -427,8 +428,9 @@ export class LoadedScriptsView extends ViewPane { @ILabelService private readonly labelService: ILabelService, @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, + @ITelemetryService telemetryService: ITelemetryService, ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('loadedScriptsSection', "Loaded Scripts Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('loadedScriptsSection', "Loaded Scripts Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.loadedScriptsItemType = CONTEXT_LOADED_SCRIPTS_ITEM_TYPE.bindTo(contextKeyService); } diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index 338d74cc951..b803f4ce468 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -58,6 +58,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IViewsService, IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ReplGroup } from 'vs/workbench/contrib/debug/common/replModel'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; const $ = dom.$; @@ -100,7 +101,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { @IStorageService private readonly storageService: IStorageService, @IThemeService themeService: IThemeService, @IModelService private readonly modelService: IModelService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IContextKeyService contextKeyService: IContextKeyService, @ICodeEditorService codeEditorService: ICodeEditorService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IContextMenuService contextMenuService: IContextMenuService, @@ -110,8 +111,9 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { @IEditorService private readonly editorService: IEditorService, @IKeybindingService keybindingService: IKeybindingService, @IOpenerService openerService: IOpenerService, + @ITelemetryService telemetryService: ITelemetryService, ) { - super({ ...(options as IViewPaneOptions), id: REPL_VIEW_ID, ariaHeaderLabel: localize('debugConsole', "Debug Console") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); + super({ ...(options as IViewPaneOptions), id: REPL_VIEW_ID, ariaHeaderLabel: localize('debugConsole', "Debug Console") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.history = new HistoryNavigator(JSON.parse(this.storageService.get(HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]')), 50); codeEditorService.registerDecorationType(DECORATION_KEY, {}); diff --git a/src/vs/workbench/contrib/debug/browser/startView.ts b/src/vs/workbench/contrib/debug/browser/startView.ts index 89a25705c94..38ef45e83b5 100644 --- a/src/vs/workbench/contrib/debug/browser/startView.ts +++ b/src/vs/workbench/contrib/debug/browser/startView.ts @@ -21,14 +21,20 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { WorkbenchStateContext } from 'vs/workbench/browser/contextkeys'; import { OpenFolderAction, OpenFileAction, OpenFileFolderAction } from 'vs/workbench/browser/actions/workspaceActions'; import { isMacintosh } from 'vs/base/common/platform'; +import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -const CONTEXT_DEBUGGER_INTERESTED = new RawContextKey('debuggerInterested', false); +const debugStartLanguageKey = 'debugStartLanguage'; +const CONTEXT_DEBUG_START_LANGUAGE = new RawContextKey(debugStartLanguageKey, undefined); +const CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR = new RawContextKey('debuggerInterestedInActiveEditor', false); export class StartView extends ViewPane { static ID = 'workbench.debug.startView'; static LABEL = localize('start', "Start"); + private debugStartLanguageContext: IContextKey; private debuggerInterestedContext: IContextKey; constructor( @@ -43,17 +49,36 @@ export class StartView extends ViewPane { @IInstantiationService instantiationService: IInstantiationService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IOpenerService openerService: IOpenerService, + @IStorageService storageSevice: IStorageService, + @ITelemetryService telemetryService: ITelemetryService, ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: localize('debugStart', "Debug Start Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: localize('debugStart', "Debug Start Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + + this.debugStartLanguageContext = CONTEXT_DEBUG_START_LANGUAGE.bindTo(contextKeyService); + this.debuggerInterestedContext = CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR.bindTo(contextKeyService); + const lastSetLanguage = storageSevice.get(debugStartLanguageKey, StorageScope.WORKSPACE); + this.debugStartLanguageContext.set(lastSetLanguage); - this.debuggerInterestedContext = CONTEXT_DEBUGGER_INTERESTED.bindTo(contextKeyService); const setContextKey = () => { - const activeEditor = this.editorService.activeTextEditorWidget; - const debuggerLabels = this.debugService.getConfigurationManager().getDebuggerLabelsForEditor(activeEditor); - this.debuggerInterestedContext.set(debuggerLabels.length > 0); + const editor = this.editorService.activeTextEditorWidget; + if (isCodeEditor(editor)) { + const model = editor.getModel(); + const language = model ? model.getLanguageIdentifier().language : undefined; + if (language && this.debugService.getConfigurationManager().isDebuggerInterestedInLanguage(language)) { + this.debugStartLanguageContext.set(language); + this.debuggerInterestedContext.set(true); + storageSevice.store(debugStartLanguageKey, language, StorageScope.WORKSPACE); + return; + } + } + this.debuggerInterestedContext.set(false); }; this._register(editorService.onDidActiveEditorChange(setContextKey)); this._register(this.debugService.getConfigurationManager().onDidRegisterDebugger(setContextKey)); + setContextKey(); + + const debugKeybinding = this.keybindingService.lookupKeybinding(StartAction.ID); + debugKeybindingLabel = debugKeybinding ? ` (${debugKeybinding.getLabel()})` : ''; } shouldShowWelcome(): boolean { @@ -64,11 +89,13 @@ export class StartView extends ViewPane { const viewsRegistry = Registry.as(Extensions.ViewsRegistry); viewsRegistry.registerViewWelcomeContent(StartView.ID, { content: localize('openAFileWhichCanBeDebugged', "[Open a file](command:{0}) which can be debugged or run.", isMacintosh ? OpenFileFolderAction.ID : OpenFileAction.ID), - when: CONTEXT_DEBUGGER_INTERESTED.toNegated() + when: CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR.toNegated() }); +let debugKeybindingLabel = ''; viewsRegistry.registerViewWelcomeContent(StartView.ID, { - content: localize('runAndDebugAction', "[Run and Debug](command:{0})", StartAction.ID) + content: localize('runAndDebugAction', "[Run and Debug{0}](command:{1})", debugKeybindingLabel, StartAction.ID), + preconditions: [CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR] }); viewsRegistry.registerViewWelcomeContent(StartView.ID, { diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index 4dd9c4bfac0..2c40b45a4e2 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -33,6 +33,7 @@ import { dispose } from 'vs/base/common/lifecycle'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; const $ = dom.$; let forgetScopes = true; @@ -58,8 +59,9 @@ export class VariablesView extends ViewPane { @IContextKeyService contextKeyService: IContextKeyService, @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, + @ITelemetryService telemetryService: ITelemetryService, ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('variablesSection', "Variables Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('variablesSection', "Variables Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); // Use scheduler to prevent unnecessary flashing this.onFocusStackFrameScheduler = new RunOnceScheduler(async () => { diff --git a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts index 99b9cc6d32e..1be23a91b3e 100644 --- a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts +++ b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts @@ -33,6 +33,7 @@ import { dispose } from 'vs/base/common/lifecycle'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024; let ignoreVariableSetEmitter = false; @@ -55,8 +56,9 @@ export class WatchExpressionsView extends ViewPane { @IContextKeyService contextKeyService: IContextKeyService, @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, + @ITelemetryService telemetryService: ITelemetryService, ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('watchExpressionsSection', "Watch Expressions Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('watchExpressionsSection', "Watch Expressions Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.onWatchExpressionsUpdatedScheduler = new RunOnceScheduler(() => { this.needsRefresh = false; diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 9a9fbceb7bf..d753ab2f888 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -647,7 +647,7 @@ export interface IConfigurationManager { activateDebuggers(activationEvent: string, debugType?: string): Promise; - getDebuggerLabelsForEditor(editor: editorCommon.IEditor | undefined): string[]; + isDebuggerInterestedInLanguage(language: string): boolean; hasDebugConfigurationProvider(debugType: string): boolean; registerDebugConfigurationProvider(debugConfigurationProvider: IDebugConfigurationProvider): IDisposable; diff --git a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts index d6ad9e7fbf2..83b69ec0ed7 100644 --- a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts @@ -17,6 +17,7 @@ import { LanguageIdentifier, LanguageId } from 'vs/editor/common/modes'; import { createBreakpointDecorations } from 'vs/workbench/contrib/debug/browser/breakpointEditorContribution'; import { OverviewRulerLane } from 'vs/editor/common/model'; import { MarkdownString } from 'vs/base/common/htmlContent'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; function createMockSession(model: DebugModel, name = 'mockSession', options?: IDebugSessionOptions): DebugSession { return new DebugSession({ resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, options, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!, undefined!); @@ -319,7 +320,7 @@ suite('Debug - Breakpoints', () => { test('decorations', () => { const modelUri = uri.file('/myfolder/my file first.js'); const languageIdentifier = new LanguageIdentifier('testMode', LanguageId.PlainText); - const textModel = TextModel.createFromString( + const textModel = createTextModel( ['this is line one', 'this is line two', ' this is line three it has whitespace at start', 'this is line four', 'this is line five'].join('\n'), TextModel.DEFAULT_CREATION_OPTIONS, languageIdentifier diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index be1a3fa8093..39ada55824f 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -1060,7 +1060,7 @@ export class ExtensionEditor extends BaseEditor { } private renderCustomEditors(container: HTMLElement, manifest: IExtensionManifest, onDetailsToggle: Function): boolean { - const webviewEditors = manifest.contributes?.webviewEditors || []; + const webviewEditors = manifest.contributes?.customEditors || []; if (!webviewEditors.length) { return false; } diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 8f0b1969062..cf17ac8d9c5 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -87,7 +87,8 @@ Registry.as(ViewContainerExtensions.ViewContainersRegis name: localize('extensions', "Extensions"), ctorDescriptor: new SyncDescriptor(ExtensionsViewPaneContainer), icon: 'codicon-extensions', - order: 4 + order: 4, + rejectAddedViews: true, }, ViewContainerLocation.Sidebar); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 0821aeea684..52378063e9c 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -102,19 +102,19 @@ export class ExtensionsListView extends ViewPane { @IExtensionsWorkbenchService protected extensionsWorkbenchService: IExtensionsWorkbenchService, @IEditorService private readonly editorService: IEditorService, @IExtensionTipsService protected tipsService: IExtensionTipsService, - @ITelemetryService private readonly telemetryService: ITelemetryService, + @ITelemetryService telemetryService: ITelemetryService, @IConfigurationService configurationService: IConfigurationService, @IWorkspaceContextService protected contextService: IWorkspaceContextService, @IExperimentService private readonly experimentService: IExperimentService, @IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService, @IExtensionManagementServerService protected readonly extensionManagementServerService: IExtensionManagementServerService, @IProductService protected readonly productService: IProductService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IContextKeyService contextKeyService: IContextKeyService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IMenuService private readonly menuService: IMenuService, @IOpenerService openerService: IOpenerService, ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: options.title, showActionsAlways: true }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: options.title, showActionsAlways: true }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.server = options.server; } diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css index 8e62f3a97be..f61faa096cb 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css @@ -154,6 +154,7 @@ .extensions-viewlet > .extensions .monaco-list-row > .extension > .details > .header-container > .header .extension-remote-badge > .codicon { font-size: 12px; + color: currentColor; } .extensions-viewlet.narrow > .extensions .extension > .icon-container, diff --git a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts index 720b01e66ed..ba4f2cae30f 100644 --- a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts +++ b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts @@ -65,21 +65,21 @@ export class ExplorerViewletViewsContribution extends Disposable implements IWor private registerViews(): void { const viewsRegistry = Registry.as(Extensions.ViewsRegistry); - viewsRegistry.registerViewWelcomeContent(EmptyView.ID, { + this._register(viewsRegistry.registerViewWelcomeContent(EmptyView.ID, { content: localize('noWorkspaceHelp', "You have not yet added a folder to the workspace.\n[Add Folder](command:{0})", AddRootFolderAction.ID), when: WorkbenchStateContext.isEqualTo('workspace') - }); + })); const commandId = isMacintosh ? OpenFileFolderAction.ID : OpenFolderAction.ID; - viewsRegistry.registerViewWelcomeContent(EmptyView.ID, { + this._register(viewsRegistry.registerViewWelcomeContent(EmptyView.ID, { content: localize('remoteNoFolderHelp', "Connected to remote.\n[Open Folder](command:{0})", commandId), when: ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('workspace'), RemoteNameContext.notEqualsTo(''), IsWebContext.toNegated()) - }); + })); - viewsRegistry.registerViewWelcomeContent(EmptyView.ID, { + this._register(viewsRegistry.registerViewWelcomeContent(EmptyView.ID, { content: localize('noFolderHelp', "You have not yet opened a folder.\n[Open Folder](command:{0})", commandId), when: ContextKeyExpr.or(ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('workspace'), RemoteNameContext.isEqualTo('')), ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('workspace'), IsWebContext)) - }); + })); const viewDescriptors = viewsRegistry.getViews(VIEW_CONTAINER); diff --git a/src/vs/workbench/contrib/files/browser/views/emptyView.ts b/src/vs/workbench/contrib/files/browser/views/emptyView.ts index e9296281846..212fe31a042 100644 --- a/src/vs/workbench/contrib/files/browser/views/emptyView.ts +++ b/src/vs/workbench/contrib/files/browser/views/emptyView.ts @@ -19,6 +19,7 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; export class EmptyView extends ViewPane { @@ -36,9 +37,10 @@ export class EmptyView extends ViewPane { @IConfigurationService configurationService: IConfigurationService, @ILabelService private labelService: ILabelService, @IContextKeyService contextKeyService: IContextKeyService, - @IOpenerService openerService: IOpenerService + @IOpenerService openerService: IOpenerService, + @ITelemetryService telemetryService: ITelemetryService, ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('explorerSection', "Explorer Section: No Folder Opened") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('explorerSection', "Explorer Section: No Folder Opened") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this._register(this.contextService.onDidChangeWorkbenchState(() => this.refreshTitle())); this._register(this.labelService.onDidChangeFormatters(() => this.refreshTitle())); diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 1af95f4a23c..3e5fc89ec88 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -159,20 +159,20 @@ export class ExplorerView extends ViewPane { @IEditorService private readonly editorService: IEditorService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IKeybindingService keybindingService: IKeybindingService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IContextKeyService contextKeyService: IContextKeyService, @IConfigurationService configurationService: IConfigurationService, @IDecorationsService private readonly decorationService: IDecorationsService, @ILabelService private readonly labelService: ILabelService, @IThemeService protected themeService: IWorkbenchThemeService, @IMenuService private readonly menuService: IMenuService, - @ITelemetryService private readonly telemetryService: ITelemetryService, + @ITelemetryService telemetryService: ITelemetryService, @IExplorerService private readonly explorerService: IExplorerService, @IStorageService private readonly storageService: IStorageService, @IClipboardService private clipboardService: IClipboardService, @IFileService private readonly fileService: IFileService, @IOpenerService openerService: IOpenerService, ) { - super({ ...(options as IViewPaneOptions), id: ExplorerView.ID, ariaHeaderLabel: nls.localize('explorerSection', "Explorer Section: {0}", labelService.getWorkspaceLabel(contextService.getWorkspace())) }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); + super({ ...(options as IViewPaneOptions), id: ExplorerView.ID, ariaHeaderLabel: nls.localize('explorerSection', "Explorer Section: {0}", labelService.getWorkspaceLabel(contextService.getWorkspace())) }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.resourceContext = instantiationService.createInstance(ResourceContextKey); this._register(this.resourceContext); diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index da798f2d1a8..c388727aba4 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -76,9 +76,9 @@ export class OpenEditorsView extends ViewPane { @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IConfigurationService configurationService: IConfigurationService, @IKeybindingService keybindingService: IKeybindingService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IContextKeyService contextKeyService: IContextKeyService, @IThemeService themeService: IThemeService, - @ITelemetryService private readonly telemetryService: ITelemetryService, + @ITelemetryService telemetryService: ITelemetryService, @IMenuService private readonly menuService: IMenuService, @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, @@ -87,7 +87,7 @@ export class OpenEditorsView extends ViewPane { super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize({ key: 'openEditosrSection', comment: ['Open is an adjective'] }, "Open Editors Section"), - }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); + }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.structuralRefreshDelay = 0; this.listRefreshScheduler = new RunOnceScheduler(() => { diff --git a/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts b/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts index 93617ce7314..4c12705aa05 100644 --- a/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts @@ -7,9 +7,8 @@ import * as assert from 'assert'; import { Event } from 'vs/base/common/event'; import { toResource } from 'vs/base/test/common/utils'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { TestFileService, TestFilesConfigurationService, TestEnvironmentService, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; -import { ITextFileService, IResolvedTextFileEditorModel, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; -import { IFileService } from 'vs/platform/files/common/files'; +import { TestFilesConfigurationService, TestEnvironmentService, workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; +import { IResolvedTextFileEditorModel, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; @@ -28,17 +27,6 @@ import { IFilesConfigurationService } from 'vs/workbench/services/filesConfigura import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; -class ServiceAccessor { - constructor( - @IEditorService public editorService: IEditorService, - @IEditorGroupsService public editorGroupService: IEditorGroupsService, - @ITextFileService public textFileService: ITextFileService, - @IFileService public fileService: TestFileService, - @IConfigurationService public configurationService: TestConfigurationService - ) { - } -} - suite('EditorAutoSave', () => { let disposables: IDisposable[] = []; @@ -81,7 +69,7 @@ suite('EditorAutoSave', () => { const editorService: EditorService = instantiationService.createInstance(EditorService); instantiationService.stub(IEditorService, editorService); - const accessor = instantiationService.createInstance(ServiceAccessor); + const accessor = instantiationService.createInstance(TestServiceAccessor); const editorAutoSave = instantiationService.createInstance(EditorAutoSave); diff --git a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts index 2ff0f6bde41..9aaea5c645b 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts @@ -7,34 +7,23 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { toResource } from 'vs/base/test/common/utils'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { workbenchInstantiationService, TestTextFileService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { EncodingMode, Verbosity } from 'vs/workbench/common/editor'; -import { ITextFileService, TextFileOperationError, TextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; +import { TextFileOperationError, TextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; import { FileOperationResult, FileOperationError } from 'vs/platform/files/common/files'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; -import { IModelService } from 'vs/editor/common/services/modelService'; import { timeout } from 'vs/base/common/async'; import { ModesRegistry, PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; import { DisposableStore } from 'vs/base/common/lifecycle'; -class ServiceAccessor { - constructor( - @IEditorService public editorService: IEditorService, - @ITextFileService public textFileService: TestTextFileService, - @IModelService public modelService: IModelService - ) { - } -} - suite('Files - FileEditorInput', () => { let instantiationService: IInstantiationService; - let accessor: ServiceAccessor; + let accessor: TestServiceAccessor; setup(() => { instantiationService = workbenchInstantiationService(); - accessor = instantiationService.createInstance(ServiceAccessor); + accessor = instantiationService.createInstance(TestServiceAccessor); }); test('Basics', async function () { diff --git a/src/vs/workbench/contrib/files/test/browser/fileEditorTracker.test.ts b/src/vs/workbench/contrib/files/test/browser/fileEditorTracker.test.ts index 026624314a0..d2b3385e6df 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileEditorTracker.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileEditorTracker.test.ts @@ -8,9 +8,9 @@ import { Event } from 'vs/base/common/event'; import { TextFileEditorTracker } from 'vs/workbench/contrib/files/browser/editors/textFileEditorTracker'; import { toResource } from 'vs/base/test/common/utils'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { TestFileService, TestTextFileService, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; -import { ITextFileService, IResolvedTextFileEditorModel, snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; -import { FileChangesEvent, FileChangeType, IFileService } from 'vs/platform/files/common/files'; +import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; +import { IResolvedTextFileEditorModel, snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; +import { FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { timeout } from 'vs/base/common/async'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; @@ -26,16 +26,6 @@ import { EditorService } from 'vs/workbench/services/editor/browser/editorServic import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; -class ServiceAccessor { - constructor( - @IEditorService public editorService: IEditorService, - @IEditorGroupsService public editorGroupService: IEditorGroupsService, - @ITextFileService public textFileService: TestTextFileService, - @IFileService public fileService: TestFileService - ) { - } -} - suite('Files - TextFileEditorTracker', () => { let disposables: IDisposable[] = []; @@ -58,7 +48,7 @@ suite('Files - TextFileEditorTracker', () => { test('file change event updates model', async function () { const instantiationService = workbenchInstantiationService(); - const accessor = instantiationService.createInstance(ServiceAccessor); + const accessor = instantiationService.createInstance(TestServiceAccessor); const tracker = instantiationService.createInstance(TextFileEditorTracker); @@ -82,7 +72,7 @@ suite('Files - TextFileEditorTracker', () => { (accessor.textFileService.files).dispose(); }); - async function createTracker(): Promise<[EditorPart, ServiceAccessor, TextFileEditorTracker, IInstantiationService, IEditorService]> { + async function createTracker(): Promise<[EditorPart, TestServiceAccessor, TextFileEditorTracker, IInstantiationService, IEditorService]> { const instantiationService = workbenchInstantiationService(); const part = instantiationService.createInstance(EditorPart); @@ -94,7 +84,7 @@ suite('Files - TextFileEditorTracker', () => { const editorService: EditorService = instantiationService.createInstance(EditorService); instantiationService.stub(IEditorService, editorService); - const accessor = instantiationService.createInstance(ServiceAccessor); + const accessor = instantiationService.createInstance(TestServiceAccessor); await part.whenRestored; diff --git a/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts b/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts index 18735013da0..6b29c6e66d9 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts @@ -5,27 +5,19 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; -import { workbenchInstantiationService, TestFileService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TextFileContentProvider } from 'vs/workbench/contrib/files/common/files'; import { snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; -import { IFileService } from 'vs/platform/files/common/files'; - -class ServiceAccessor { - constructor( - @IFileService public fileService: TestFileService - ) { - } -} suite('Files - FileOnDiskContentProvider', () => { let instantiationService: IInstantiationService; - let accessor: ServiceAccessor; + let accessor: TestServiceAccessor; setup(() => { instantiationService = workbenchInstantiationService(); - accessor = instantiationService.createInstance(ServiceAccessor); + accessor = instantiationService.createInstance(TestServiceAccessor); }); test('provideTextContent', async () => { diff --git a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts index 55627367a15..abd5d1d00fe 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts +++ b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts @@ -51,6 +51,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { domEvent } from 'vs/base/browser/event'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; +import { Progress } from 'vs/platform/progress/common/progress'; export type TreeElement = ResourceMarkers | Marker | RelatedInformation; @@ -642,7 +643,7 @@ export class MarkerViewModel extends Disposable { this.codeActionsPromise = createCancelablePromise(cancellationToken => { return getCodeActions(model, new Range(this.marker.range.startLineNumber, this.marker.range.startColumn, this.marker.range.endLineNumber, this.marker.range.endColumn), { type: CodeActionTriggerType.Manual, filter: { include: CodeActionKind.QuickFix } - }, cancellationToken).then(actions => { + }, Progress.None, cancellationToken).then(actions => { return this._register(actions); }); }); diff --git a/src/vs/workbench/contrib/markers/browser/markersView.ts b/src/vs/workbench/contrib/markers/browser/markersView.ts index 008b211f282..bb4a5763759 100644 --- a/src/vs/workbench/contrib/markers/browser/markersView.ts +++ b/src/vs/workbench/contrib/markers/browser/markersView.ts @@ -97,7 +97,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IEditorService private readonly editorService: IEditorService, @IConfigurationService configurationService: IConfigurationService, - @ITelemetryService private readonly telemetryService: ITelemetryService, + @ITelemetryService telemetryService: ITelemetryService, @IMarkersWorkbenchService private readonly markersWorkbenchService: IMarkersWorkbenchService, @IContextKeyService contextKeyService: IContextKeyService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @@ -108,7 +108,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, ) { - super({ ...(options as IViewPaneOptions), id: Constants.MARKERS_VIEW_ID, ariaHeaderLabel: Messages.MARKERS_PANEL_TITLE_PROBLEMS }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); + super({ ...(options as IViewPaneOptions), id: Constants.MARKERS_VIEW_ID, ariaHeaderLabel: Messages.MARKERS_PANEL_TITLE_PROBLEMS }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.panelFoucusContextKey = Constants.MarkerViewFocusContextKey.bindTo(contextKeyService); this.panelState = new Memento(Constants.MARKERS_VIEW_STORAGE_ID, storageService).getMemento(StorageScope.WORKSPACE); this.markersViewModel = this._register(instantiationService.createInstance(MarkersViewModel, this.panelState['multiline'])); @@ -349,7 +349,6 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { private createArialLabelElement(parent: HTMLElement): void { this.ariaLabelElement = dom.append(parent, dom.$('')); this.ariaLabelElement.setAttribute('id', 'markers-panel-arialabel'); - this.ariaLabelElement.setAttribute('aria-live', 'polite'); } private createTree(parent: HTMLElement): void { diff --git a/src/vs/workbench/contrib/outline/browser/outlinePane.ts b/src/vs/workbench/contrib/outline/browser/outlinePane.ts index 148a64dda65..906dc9572d8 100644 --- a/src/vs/workbench/contrib/outline/browser/outlinePane.ts +++ b/src/vs/workbench/contrib/outline/browser/outlinePane.ts @@ -50,6 +50,7 @@ import { MarkerSeverity } from 'vs/platform/markers/common/markers'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; class RequestState { @@ -270,8 +271,9 @@ export class OutlinePane extends ViewPane { @IContextMenuService contextMenuService: IContextMenuService, @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, + @ITelemetryService telemetryService: ITelemetryService, ) { - super(options, keybindingService, contextMenuService, _configurationService, contextKeyService, viewDescriptorService, _instantiationService, openerService, themeService); + super(options, keybindingService, contextMenuService, _configurationService, contextKeyService, viewDescriptorService, _instantiationService, openerService, themeService, telemetryService); this._outlineViewState.restore(this._storageService); this._contextKeyFocused = OutlineViewFocused.bindTo(contextKeyService); this._contextKeyFiltered = OutlineViewFiltered.bindTo(contextKeyService); diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index f5cc76f8b4d..aebdb85e8b8 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -43,14 +43,15 @@ export class OutputViewPane extends ViewPane { @IKeybindingService keybindingService: IKeybindingService, @IContextMenuService contextMenuService: IContextMenuService, @IConfigurationService configurationService: IConfigurationService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IContextKeyService contextKeyService: IContextKeyService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IInstantiationService instantiationService: IInstantiationService, @IOutputService private readonly outputService: IOutputService, @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, + @ITelemetryService telemetryService: ITelemetryService, ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.editor = instantiationService.createInstance(OutputEditor); this._register(this.editor.onTitleAreaUpdate(() => { this.updateTitle(this.editor.getTitle()); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 392db681d09..30eb7a13953 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -77,7 +77,7 @@ export class SettingsEditor2 extends BaseEditor { private static CONFIG_SCHEMA_UPDATE_DELAYER = 500; private static readonly SUGGESTIONS: string[] = [ - `@${MODIFIED_SETTING_TAG}`, '@tag:usesOnlineServices', `@${EXTENSION_SETTING_TAG}` + `@${MODIFIED_SETTING_TAG}`, '@tag:usesOnlineServices', '@tag:sync', `@${EXTENSION_SETTING_TAG}` ]; private static shouldSettingUpdateFast(type: SettingValueType | SettingValueType[]): boolean { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index 8d1f6e2ab07..c1a674828f9 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -49,7 +49,7 @@ import { ExcludeSettingWidget, IListChangeEvent, IListDataItem, ListSettingWidge import { SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU } from 'vs/workbench/contrib/preferences/common/preferences'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ISetting, ISettingsGroup, SettingValueType } from 'vs/workbench/services/preferences/common/preferences'; -import { IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncEnablementService, getDefaultIgnoredSettings } from 'vs/platform/userDataSync/common/userDataSync'; const $ = DOM.$; @@ -1238,7 +1238,7 @@ export class SettingTreeRenderers { private getActionsForSetting(setting: ISetting): IAction[] { const enableSync = this._userDataSyncEnablementService.isEnabled(); return enableSync && !setting.disallowSyncIgnore ? - [this._instantiationService.createInstance(StopSyncingSettingAction, setting)] : + [this._instantiationService.createInstance(SyncSettingAction, setting)] : []; } @@ -1622,7 +1622,7 @@ class CopySettingAsJSONAction extends Action { } } -class StopSyncingSettingAction extends Action { +class SyncSettingAction extends Action { static readonly ID = 'settings.stopSyncingSetting'; static readonly LABEL = localize('stopSyncingSetting', "Sync This Setting"); @@ -1630,24 +1630,38 @@ class StopSyncingSettingAction extends Action { private readonly setting: ISetting, @IConfigurationService private readonly configService: IConfigurationService, ) { - super(StopSyncingSettingAction.ID, StopSyncingSettingAction.LABEL); + super(SyncSettingAction.ID, SyncSettingAction.LABEL); + this._register(Event.filter(configService.onDidChangeConfiguration, e => e.affectsConfiguration('sync.ignoredSettings'))(() => this.update())); this.update(); } - update() { - const ignoredSettings = getIgnoredSettings(this.configService); + async update() { + const ignoredSettings = getIgnoredSettings(getDefaultIgnoredSettings(), this.configService); this.checked = !ignoredSettings.includes(this.setting.key); } async run(): Promise { + // first remove the current setting completely from ignored settings let currentValue = [...this.configService.getValue('sync.ignoredSettings')]; - if (this.checked) { - currentValue.push(this.setting.key); - } else { - currentValue = currentValue.filter(v => v !== this.setting.key); + currentValue = currentValue.filter(v => v !== this.setting.key && v !== `-${this.setting.key}`); + + const defaultIgnoredSettings = getDefaultIgnoredSettings(); + const isDefaultIgnored = defaultIgnoredSettings.includes(this.setting.key); + const askedToSync = !this.checked; + + // If asked to sync, then add only if it is ignored by default + if (askedToSync && isDefaultIgnored) { + currentValue.push(`-${this.setting.key}`); } + + // If asked not to sync, then add only if it is not ignored by default + if (!askedToSync && !isDefaultIgnored) { + currentValue.push(this.setting.key); + } + this.configService.updateValue('sync.ignoredSettings', currentValue.length ? currentValue : undefined, ConfigurationTarget.USER); return Promise.resolve(undefined); } + } diff --git a/src/vs/workbench/contrib/preferences/test/common/smartSnippetInserter.test.ts b/src/vs/workbench/contrib/preferences/test/common/smartSnippetInserter.test.ts index 90a2e3a7bbb..16c83919b14 100644 --- a/src/vs/workbench/contrib/preferences/test/common/smartSnippetInserter.test.ts +++ b/src/vs/workbench/contrib/preferences/test/common/smartSnippetInserter.test.ts @@ -5,13 +5,13 @@ import * as assert from 'assert'; import { SmartSnippetInserter } from 'vs/workbench/contrib/preferences/common/smartSnippetInserter'; -import { TextModel } from 'vs/editor/common/model/textModel'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { Position } from 'vs/editor/common/core/position'; suite('SmartSnippetInserter', () => { function testSmartSnippetInserter(text: string[], runner: (assert: (desiredPos: Position, pos: Position, prepend: string, append: string) => void) => void): void { - let model = TextModel.createFromString(text.join('\n')); + let model = createTextModel(text.join('\n')); runner((desiredPos, pos, prepend, append) => { let actual = SmartSnippetInserter.insertSnippet(model, desiredPos); let expected = { diff --git a/src/vs/workbench/contrib/remote/browser/remote.ts b/src/vs/workbench/contrib/remote/browser/remote.ts index 76b780adca8..47dd085867b 100644 --- a/src/vs/workbench/contrib/remote/browser/remote.ts +++ b/src/vs/workbench/contrib/remote/browser/remote.ts @@ -374,8 +374,9 @@ class HelpPanel extends ViewPane { @IRemoteExplorerService protected readonly remoteExplorerService: IRemoteExplorerService, @IWorkbenchEnvironmentService protected readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, @IThemeService themeService: IThemeService, + @ITelemetryService telemetryService: ITelemetryService, ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); } protected renderBody(container: HTMLElement): void { diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 664565a92cf..f4768fd7a15 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -40,6 +40,7 @@ import { URI } from 'vs/base/common/uri'; import { RemoteTunnel } from 'vs/platform/remote/common/tunnel'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; export const forwardedPortsViewEnabled = new RawContextKey('forwardedPortsViewEnabled', false); @@ -457,9 +458,10 @@ export class TunnelPanel extends ViewPane { @INotificationService private readonly notificationService: INotificationService, @IContextViewService private readonly contextViewService: IContextViewService, @IThemeService themeService: IThemeService, - @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService + @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService, + @ITelemetryService telemetryService: ITelemetryService, ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.tunnelTypeContext = TunnelTypeContextKey.bindTo(contextKeyService); this.tunnelCloseableContext = TunnelCloseableContextKey.bindTo(contextKeyService); this.tunnelViewFocusContext = TunnelViewFocusContextKey.bindTo(contextKeyService); diff --git a/src/vs/workbench/contrib/scm/browser/mainPane.ts b/src/vs/workbench/contrib/scm/browser/mainPane.ts index a1219d0d8c2..60d2d14c614 100644 --- a/src/vs/workbench/contrib/scm/browser/mainPane.ts +++ b/src/vs/workbench/contrib/scm/browser/mainPane.ts @@ -33,6 +33,7 @@ import { IViewDescriptor, IViewDescriptorService } from 'vs/workbench/common/vie import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; export interface ISpliceEvent { index: number; @@ -185,13 +186,14 @@ export class MainPane extends ViewPane { @IContextMenuService protected contextMenuService: IContextMenuService, @IInstantiationService instantiationService: IInstantiationService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IContextKeyService contextKeyService: IContextKeyService, @IMenuService private readonly menuService: IMenuService, @IConfigurationService configurationService: IConfigurationService, @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, + @ITelemetryService telemetryService: ITelemetryService, ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); } protected renderBody(container: HTMLElement): void { diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPane.ts b/src/vs/workbench/contrib/scm/browser/repositoryPane.ts index c5b5c59c295..e13d4f79beb 100644 --- a/src/vs/workbench/contrib/scm/browser/repositoryPane.ts +++ b/src/vs/workbench/contrib/scm/browser/repositoryPane.ts @@ -68,6 +68,7 @@ import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2 import { Schemas } from 'vs/base/common/network'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; type TreeElement = ISCMResourceGroup | IResourceNode | ISCMResource; @@ -620,8 +621,6 @@ export class RepositoryPane extends ViewPane { protected contextKeyService: IContextKeyService; private commitTemplate = ''; - shouldShowWelcome() { return true; } - constructor( readonly repository: ISCMRepository, options: IViewPaneOptions, @@ -640,8 +639,9 @@ export class RepositoryPane extends ViewPane { @IStorageService private storageService: IStorageService, @IModelService private modelService: IModelService, @IOpenerService openerService: IOpenerService, + @ITelemetryService telemetryService: ITelemetryService, ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.menus = instantiationService.createInstance(SCMMenus, this.repository.provider); this._register(this.menus); diff --git a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts index 035f4617e54..b78b35610e7 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts @@ -56,8 +56,9 @@ export class EmptyPane extends ViewPane { @IInstantiationService instantiationService: IInstantiationService, @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, + @ITelemetryService telemetryService: ITelemetryService, ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); } shouldShowWelcome(): boolean { diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index 97da9089fd0..2bae8bb20ce 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -54,7 +54,6 @@ import { ExplorerViewPaneContainer } from 'vs/workbench/contrib/files/browser/ex import { assertType, assertIsDefined } from 'vs/base/common/types'; import { SearchViewPaneContainer } from 'vs/workbench/contrib/search/browser/searchViewlet'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import product from 'vs/platform/product/common/product'; import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor'; registerSingleton(ISearchWorkbenchService, SearchWorkbenchService, true); @@ -557,8 +556,8 @@ MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, { order: 1 }); -registry.registerWorkbenchAction(SyncActionDescriptor.create(FocusNextSearchResultAction, FocusNextSearchResultAction.ID, FocusNextSearchResultAction.LABEL, { primary: KeyCode.F4 }, ContextKeyExpr.and(Constants.HasSearchResults)), 'Focus Next Search Result', category); -registry.registerWorkbenchAction(SyncActionDescriptor.create(FocusPreviousSearchResultAction, FocusPreviousSearchResultAction.ID, FocusPreviousSearchResultAction.LABEL, { primary: KeyMod.Shift | KeyCode.F4 }, ContextKeyExpr.and(Constants.HasSearchResults)), 'Focus Previous Search Result', category); +registry.registerWorkbenchAction(SyncActionDescriptor.create(FocusNextSearchResultAction, FocusNextSearchResultAction.ID, FocusNextSearchResultAction.LABEL, { primary: KeyCode.F4 }, ContextKeyExpr.or(Constants.HasSearchResults, SearchEditorConstants.InSearchEditor)), 'Focus Next Search Result', category); +registry.registerWorkbenchAction(SyncActionDescriptor.create(FocusPreviousSearchResultAction, FocusPreviousSearchResultAction.ID, FocusPreviousSearchResultAction.LABEL, { primary: KeyMod.Shift | KeyCode.F4 }, ContextKeyExpr.or(Constants.HasSearchResults, SearchEditorConstants.InSearchEditor)), 'Focus Previous Search Result', category); registry.registerWorkbenchAction(SyncActionDescriptor.create(ReplaceInFilesAction, ReplaceInFilesAction.ID, ReplaceInFilesAction.LABEL, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_H }), 'Replace in Files', category); MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, { @@ -775,21 +774,16 @@ configurationRegistry.registerConfiguration({ default: 300, markdownDescription: nls.localize('search.searchOnTypeDebouncePeriod', "When `#search.searchOnType#` is enabled, controls the timeout in milliseconds between a character being typed and the search starting. Has no effect when `search.searchOnType` is disabled.") }, - 'search.enableSearchEditorPreview': { - type: 'boolean', - default: product.quality !== 'stable', - description: nls.localize('search.enableSearchEditorPreview', "Experimental: When enabled, allows opening workspace search results in an editor.") - }, - 'search.searchEditorPreview.doubleClickBehaviour': { + 'search.searchEditor.doubleClickBehaviour': { type: 'string', enum: ['selectWord', 'goToLocation', 'openLocationToSide'], default: 'goToLocation', enumDescriptions: [ - nls.localize('search.searchEditorPreview.doubleClickBehaviour.selectWord', "Double clicking selects the word under the cursor."), - nls.localize('search.searchEditorPreview.doubleClickBehaviour.goToLocation', "Double clicking opens the result in the active editor group."), - nls.localize('search.searchEditorPreview.doubleClickBehaviour.openLocationToSide', "Double clicking opens the result in the editor group to the side, creating one if it does not yet exist."), + nls.localize('search.searchEditor.doubleClickBehaviour.selectWord', "Double clicking selects the word under the cursor."), + nls.localize('search.searchEditor.doubleClickBehaviour.goToLocation', "Double clicking opens the result in the active editor group."), + nls.localize('search.searchEditor.doubleClickBehaviour.openLocationToSide', "Double clicking opens the result in the editor group to the side, creating one if it does not yet exist."), ], - markdownDescription: nls.localize('search.searchEditorPreview.doubleClickBehaviour', "Configure effect of double clicking a result in a Search Editor.\n\n `#search.enableSearchEditorPreview#` must be enabled for this setting to have an effect.") + markdownDescription: nls.localize('search.searchEditor.doubleClickBehaviour', "Configure effect of double clicking a result in a search editor.") }, 'search.sortOrder': { 'type': 'string', diff --git a/src/vs/workbench/contrib/search/browser/searchActions.ts b/src/vs/workbench/contrib/search/browser/searchActions.ts index 0b16e7a65ad..574189c1a09 100644 --- a/src/vs/workbench/contrib/search/browser/searchActions.ts +++ b/src/vs/workbench/contrib/search/browser/searchActions.ts @@ -488,12 +488,19 @@ export class FocusNextSearchResultAction extends Action { static readonly LABEL = nls.localize('FocusNextSearchResult.label', "Focus Next Search Result"); constructor(id: string, label: string, - @IViewsService private readonly viewsService: IViewsService + @IViewsService private readonly viewsService: IViewsService, + @IEditorService private readonly editorService: IEditorService, ) { super(id, label); } - run(): Promise { + async run(): Promise { + const input = this.editorService.activeEditor; + if (input instanceof SearchEditorInput) { + // cast as we cannot import SearchEditor as a value b/c cyclic dependency. + return (this.editorService.activeControl as SearchEditor).focusNextResult(); + } + return openSearchView(this.viewsService).then(searchView => { if (searchView) { searchView.selectNextMatch(); @@ -507,12 +514,19 @@ export class FocusPreviousSearchResultAction extends Action { static readonly LABEL = nls.localize('FocusPreviousSearchResult.label', "Focus Previous Search Result"); constructor(id: string, label: string, - @IViewsService private readonly viewsService: IViewsService + @IViewsService private readonly viewsService: IViewsService, + @IEditorService private readonly editorService: IEditorService, ) { super(id, label); } - run(): Promise { + async run(): Promise { + const input = this.editorService.activeEditor; + if (input instanceof SearchEditorInput) { + // cast as we cannot import SearchEditor as a value b/c cyclic dependency. + return (this.editorService.activeControl as SearchEditor).focusPreviousResult(); + } + return openSearchView(this.viewsService).then(searchView => { if (searchView) { searchView.selectPreviousMatch(); diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 94530fb3543..c782c6ef98f 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -34,7 +34,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { TreeResourceNavigator, WorkbenchObjectTree, getSelectionKeyboardEvent } from 'vs/platform/list/browser/listService'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IProgressService, IProgressStep, IProgress } from 'vs/platform/progress/common/progress'; -import { IPatternInfo, ISearchComplete, ISearchConfiguration, ISearchConfigurationProperties, ITextQuery, VIEW_ID, SearchSortOrder } from 'vs/workbench/services/search/common/search'; +import { IPatternInfo, ISearchComplete, ISearchConfiguration, ISearchConfigurationProperties, ITextQuery, VIEW_ID, SearchSortOrder, SearchCompletionExitCode } from 'vs/workbench/services/search/common/search'; import { ISearchHistoryService, ISearchHistoryValues } from 'vs/workbench/contrib/search/common/searchHistoryService'; import { diffInserted, diffInsertedOutline, diffRemoved, diffRemovedOutline, editorFindMatchHighlight, editorFindMatchHighlightBorder, listActiveSelectionForeground, foreground } from 'vs/platform/theme/common/colorRegistry'; import { ICssStyleCollector, ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; @@ -67,6 +67,7 @@ import { Color, RGBA } from 'vs/base/common/color'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { OpenSearchEditorAction, createEditorFromSearchResult } from 'vs/workbench/contrib/searchEditor/browser/searchEditorActions'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; const $ = dom.$; @@ -173,31 +174,33 @@ export class SearchView extends ViewPane { @IKeybindingService keybindingService: IKeybindingService, @IStorageService storageService: IStorageService, @IOpenerService openerService: IOpenerService, + @ITelemetryService telemetryService: ITelemetryService, ) { - super({ ...options, id: VIEW_ID, ariaHeaderLabel: nls.localize('searchView', "Search") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); + super({ ...options, id: VIEW_ID, ariaHeaderLabel: nls.localize('searchView', "Search") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.container = dom.$('.search-view'); + // globals + this.viewletVisible = Constants.SearchViewVisibleKey.bindTo(this.contextKeyService); + this.firstMatchFocused = Constants.FirstMatchFocusKey.bindTo(this.contextKeyService); + this.fileMatchOrMatchFocused = Constants.FileMatchOrMatchFocusKey.bindTo(this.contextKeyService); + this.fileMatchOrFolderMatchFocus = Constants.FileMatchOrFolderMatchFocusKey.bindTo(this.contextKeyService); + this.fileMatchOrFolderMatchWithResourceFocus = Constants.FileMatchOrFolderMatchWithResourceFocusKey.bindTo(this.contextKeyService); + this.fileMatchFocused = Constants.FileFocusKey.bindTo(this.contextKeyService); + this.folderMatchFocused = Constants.FolderFocusKey.bindTo(this.contextKeyService); + this.hasSearchResultsKey = Constants.HasSearchResults.bindTo(this.contextKeyService); + this.matchFocused = Constants.MatchFocusKey.bindTo(this.contextKeyService); + + // scoped this.contextKeyService = this._register(this.contextKeyService.createScoped(this.container)); - const viewletFocused = Constants.SearchViewFocusedKey.bindTo(this.contextKeyService); - viewletFocused.set(true); - - this.instantiationService = this.instantiationService.createChild( - new ServiceCollection([IContextKeyService, this.contextKeyService])); - - this.viewletVisible = Constants.SearchViewVisibleKey.bindTo(contextKeyService); + Constants.SearchViewFocusedKey.bindTo(this.contextKeyService).set(true); this.inputBoxFocused = Constants.InputBoxFocusedKey.bindTo(this.contextKeyService); this.inputPatternIncludesFocused = Constants.PatternIncludesFocusedKey.bindTo(this.contextKeyService); this.inputPatternExclusionsFocused = Constants.PatternExcludesFocusedKey.bindTo(this.contextKeyService); - this.firstMatchFocused = Constants.FirstMatchFocusKey.bindTo(contextKeyService); - this.fileMatchOrMatchFocused = Constants.FileMatchOrMatchFocusKey.bindTo(contextKeyService); - this.fileMatchOrFolderMatchFocus = Constants.FileMatchOrFolderMatchFocusKey.bindTo(contextKeyService); - this.fileMatchOrFolderMatchWithResourceFocus = Constants.FileMatchOrFolderMatchWithResourceFocusKey.bindTo(contextKeyService); - this.fileMatchFocused = Constants.FileFocusKey.bindTo(contextKeyService); - this.folderMatchFocused = Constants.FolderFocusKey.bindTo(contextKeyService); - this.matchFocused = Constants.MatchFocusKey.bindTo(this.contextKeyService); - this.hasSearchResultsKey = Constants.HasSearchResults.bindTo(this.contextKeyService); + + this.instantiationService = this.instantiationService.createChild( + new ServiceCollection([IContextKeyService, this.contextKeyService])); this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('search.sortOrder')) { @@ -231,14 +234,9 @@ export class SearchView extends ViewPane { this.actions = [ this._register(this.instantiationService.createInstance(ClearSearchResultsAction, ClearSearchResultsAction.ID, ClearSearchResultsAction.LABEL)), + this._register(this.instantiationService.createInstance(OpenSearchEditorAction, OpenSearchEditorAction.ID, OpenSearchEditorAction.LABEL)) ]; - if (this.searchConfig.enableSearchEditorPreview) { - this.actions.push( - this._register(this.instantiationService.createInstance(OpenSearchEditorAction, OpenSearchEditorAction.ID, OpenSearchEditorAction.LABEL)) - ); - } - this.refreshAction = this._register(this.instantiationService.createInstance(RefreshAction, RefreshAction.ID, RefreshAction.LABEL)); this.cancelAction = this._register(this.instantiationService.createInstance(CancelSearchAction, CancelSearchAction.ID, CancelSearchAction.LABEL)); this.toggleCollapseAction = this._register(this.instantiationService.createInstance(ToggleCollapseAndExpandAction, ToggleCollapseAndExpandAction.ID, ToggleCollapseAndExpandAction.LABEL, collapseDeepestExpandedLevelAction, expandAllAction)); @@ -877,7 +875,6 @@ export class SearchView extends ViewPane { this.searchWidget.setValue(selectedText); this.pauseSearching = false; updatedText = true; - if (this.searchConfig.searchOnType) { this.triggerQueryChange(); } } } @@ -1346,7 +1343,7 @@ export class SearchView extends ViewPane { this.inputPatternIncludes.onSearchSubmit(); }); - this.viewModel.cancelSearch(); + this.viewModel.cancelSearch(true); this.currentSearchQ = this.currentSearchQ .then(() => this.doSearch(query, excludePatternText, includePatternText, triggeredOnType)) @@ -1391,6 +1388,10 @@ export class SearchView extends ViewPane { this.updateActions(); const hasResults = !this.viewModel.searchResult.isEmpty(); + if (completed?.exit === SearchCompletionExitCode.NewSearchStarted) { + return; + } + if (completed && completed.limitHit) { this.searchWidget.searchInput.showMessage({ content: nls.localize('searchMaxResultsWarning', "The result set only contains a subset of all matches. Please be more specific in your search to narrow down the results."), @@ -1556,23 +1557,19 @@ export class SearchView extends ViewPane { resultMsg += nls.localize('useIgnoresAndExcludesDisabled', " - exclude settings and ignore files are disabled"); } - if (this.searchConfig.enableSearchEditorPreview) { - dom.append(messageEl, $('span', undefined, resultMsg + ' - ')); - const span = dom.append(messageEl, $('span')); - const openInEditorLink = dom.append(span, $('a.pointer.prominent', undefined, nls.localize('openInEditor.message', "Open in editor"))); + dom.append(messageEl, $('span', undefined, resultMsg + ' - ')); + const span = dom.append(messageEl, $('span')); + const openInEditorLink = dom.append(span, $('a.pointer.prominent', undefined, nls.localize('openInEditor.message', "Open in editor"))); - openInEditorLink.title = appendKeyBindingLabel( - nls.localize('openInEditor.tooltip', "Copy current search results to an editor"), - this.keybindingService.lookupKeybinding(Constants.OpenInEditorCommandId), this.keybindingService); + openInEditorLink.title = appendKeyBindingLabel( + nls.localize('openInEditor.tooltip', "Copy current search results to an editor"), + this.keybindingService.lookupKeybinding(Constants.OpenInEditorCommandId), this.keybindingService); - this.messageDisposables.push(dom.addDisposableListener(openInEditorLink, dom.EventType.CLICK, (e: MouseEvent) => { - dom.EventHelper.stop(e, false); - this.instantiationService.invokeFunction(createEditorFromSearchResult, this.searchResult, this.searchIncludePattern.getValue(), this.searchExcludePattern.getValue()); - })); + this.messageDisposables.push(dom.addDisposableListener(openInEditorLink, dom.EventType.CLICK, (e: MouseEvent) => { + dom.EventHelper.stop(e, false); + this.instantiationService.invokeFunction(createEditorFromSearchResult, this.searchResult, this.searchIncludePattern.getValue(), this.searchExcludePattern.getValue()); + })); - } else { - dom.append(messageEl, $('span', undefined, resultMsg)); - } this.reLayout(); } else if (!msgWasHidden) { dom.hide(this.messagesElement); diff --git a/src/vs/workbench/contrib/search/browser/searchWidget.ts b/src/vs/workbench/contrib/search/browser/searchWidget.ts index 863e6cbe446..428becff1e7 100644 --- a/src/vs/workbench/contrib/search/browser/searchWidget.ts +++ b/src/vs/workbench/contrib/search/browser/searchWidget.ts @@ -154,7 +154,6 @@ export class SearchWidget extends Widget { private readonly _onDidToggleContext = new Emitter(); readonly onDidToggleContext: Event = this._onDidToggleContext.event; - private temporarilySkipSearchOnChange = false; private showContextCheckbox!: Checkbox; private contextLinesInput!: InputBox; @@ -488,12 +487,10 @@ export class SearchWidget extends Widget { this.setReplaceAllActionState(false); if (this.searchConfiguration.searchOnType) { - if (!this.temporarilySkipSearchOnChange) { - this._onSearchCancel.fire({ focus: false }); - if (this.searchInput.getRegex()) { - try { - const regex = new RegExp(this.searchInput.getValue(), 'ug'); - const matchienessHeuristic = ` + if (this.searchInput.getRegex()) { + try { + const regex = new RegExp(this.searchInput.getValue(), 'ug'); + const matchienessHeuristic = ` ~!@#$%^&*()_+ \`1234567890-= qwertyuiop[]\\ @@ -503,18 +500,17 @@ export class SearchWidget extends Widget { zxcvbnm,./ ZXCVBNM<>? `.match(regex)?.length ?? 0; - const delayMultiplier = - matchienessHeuristic < 50 ? 1 : - matchienessHeuristic < 100 ? 5 : // expressions like `.` or `\w` - 10; // only things matching empty string + const delayMultiplier = + matchienessHeuristic < 50 ? 1 : + matchienessHeuristic < 100 ? 5 : // expressions like `.` or `\w` + 10; // only things matching empty string - this.submitSearch(true, this.searchConfiguration.searchOnTypeDebouncePeriod * delayMultiplier); - } catch { - // pass - } - } else { - this.submitSearch(true, this.searchConfiguration.searchOnTypeDebouncePeriod); + this.submitSearch(true, this.searchConfiguration.searchOnTypeDebouncePeriod * delayMultiplier); + } catch { + // pass } + } else { + this.submitSearch(true, this.searchConfiguration.searchOnTypeDebouncePeriod); } } } diff --git a/src/vs/workbench/contrib/search/common/searchModel.ts b/src/vs/workbench/contrib/search/common/searchModel.ts index b0c40aa7ca6..1505579db92 100644 --- a/src/vs/workbench/contrib/search/common/searchModel.ts +++ b/src/vs/workbench/contrib/search/common/searchModel.ts @@ -20,7 +20,7 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IProgress, IProgressStep } from 'vs/platform/progress/common/progress'; import { ReplacePattern } from 'vs/workbench/services/search/common/replace'; -import { IFileMatch, IPatternInfo, ISearchComplete, ISearchProgressItem, ISearchConfigurationProperties, ISearchService, ITextQuery, ITextSearchPreviewOptions, ITextSearchMatch, ITextSearchStats, resultIsMatch, ISearchRange, OneLineRange, ITextSearchContext, ITextSearchResult, SearchSortOrder } from 'vs/workbench/services/search/common/search'; +import { IFileMatch, IPatternInfo, ISearchComplete, ISearchProgressItem, ISearchConfigurationProperties, ISearchService, ITextQuery, ITextSearchPreviewOptions, ITextSearchMatch, ITextSearchStats, resultIsMatch, ISearchRange, OneLineRange, ITextSearchContext, ITextSearchResult, SearchSortOrder, SearchCompletionExitCode } from 'vs/workbench/services/search/common/search'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { overviewRulerFindMatchForeground, minimapFindMatch } from 'vs/platform/theme/common/colorRegistry'; import { themeColorFromId } from 'vs/platform/theme/common/themeService'; @@ -141,6 +141,15 @@ export class Match { return thisMatchPreviewLines.join('\n'); } + rangeInPreview() { + // convert to editor's base 1 positions. + return { + ...this._fullPreviewRange, + startColumn: this._fullPreviewRange.startColumn + 1, + endColumn: this._fullPreviewRange.endColumn + 1 + }; + } + fullPreviewLines(): string[] { return this._fullPreviewLines.slice(this._fullPreviewRange.startLineNumber, this._fullPreviewRange.endLineNumber + 1); } @@ -968,6 +977,7 @@ export class SearchModel extends Disposable { readonly onReplaceTermChanged: Event = this._onReplaceTermChanged.event; private currentCancelTokenSource: CancellationTokenSource | null = null; + private searchCancelledForNewSearch: boolean = false; constructor( @ISearchService private readonly searchService: ISearchService, @@ -1016,7 +1026,7 @@ export class SearchModel extends Disposable { } search(query: ITextQuery, onProgress?: (result: ISearchProgressItem) => void): Promise { - this.cancelSearch(); + this.cancelSearch(true); this._searchQuery = query; @@ -1114,7 +1124,12 @@ export class SearchModel extends Disposable { private onSearchError(e: any, duration: number): void { if (errors.isPromiseCanceledError(e)) { - this.onSearchCompleted(null, duration); + this.onSearchCompleted( + this.searchCancelledForNewSearch + ? { exit: SearchCompletionExitCode.NewSearchStarted, results: [] } + : null, + duration); + this.searchCancelledForNewSearch = false; } } @@ -1133,8 +1148,9 @@ export class SearchModel extends Disposable { return this.configurationService.getValue('search'); } - cancelSearch(): boolean { + cancelSearch(cancelledForNewSearch = false): boolean { if (this.currentCancelTokenSource) { + this.searchCancelledForNewSearch = cancelledForNewSearch; this.currentCancelTokenSource.cancel(); return true; } diff --git a/src/vs/workbench/contrib/searchEditor/browser/constants.ts b/src/vs/workbench/contrib/searchEditor/browser/constants.ts index f9e5cec7945..3de68eda5a0 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/constants.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/constants.ts @@ -12,10 +12,13 @@ export const ToggleSearchEditorCaseSensitiveCommandId = 'toggleSearchEditorCaseS export const ToggleSearchEditorWholeWordCommandId = 'toggleSearchEditorWholeWord'; export const ToggleSearchEditorRegexCommandId = 'toggleSearchEditorRegex'; export const ToggleSearchEditorContextLinesCommandId = 'toggleSearchEditorContextLines'; +export const RerunSearchEditorSearchCommandId = 'rerunSearchEditorSearch'; +export const CleanSearchEditorStateCommandId = 'cleanSearchEditorState'; +export const SelectAllSearchEditorMatchesCommandId = 'selectAllSearchEditorMatches'; -export const EnableSearchEditorPreview = new RawContextKey('previewSearchEditor', false); export const InSearchEditor = new RawContextKey('inSearchEditor', false); export const SearchEditorScheme = 'search-editor'; +export const SearchEditorBodyScheme = 'search-editor-body'; export const SearchEditorFindMatchClass = 'seaarchEditorFindMatch'; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts index d88a977a7b5..57b857a860d 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts @@ -10,7 +10,6 @@ import { URI } from 'vs/base/common/uri'; import { ToggleCaseSensitiveKeybinding, ToggleRegexKeybinding, ToggleWholeWordKeybinding } from 'vs/editor/contrib/find/findModel'; import { localize } from 'vs/nls'; import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -22,14 +21,15 @@ import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } fro import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { Extensions as EditorInputExtensions, IEditorInputFactory, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import * as SearchConstants from 'vs/workbench/contrib/search/common/constants'; import * as SearchEditorConstants from 'vs/workbench/contrib/searchEditor/browser/constants'; import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor'; -import { OpenResultsInEditorAction, OpenSearchEditorAction, toggleSearchEditorCaseSensitiveCommand, toggleSearchEditorContextLinesCommand, toggleSearchEditorRegexCommand, toggleSearchEditorWholeWordCommand } from 'vs/workbench/contrib/searchEditor/browser/searchEditorActions'; +import { OpenResultsInEditorAction, OpenSearchEditorAction, toggleSearchEditorCaseSensitiveCommand, toggleSearchEditorContextLinesCommand, toggleSearchEditorRegexCommand, toggleSearchEditorWholeWordCommand, selectAllSearchEditorMatchesCommand } from 'vs/workbench/contrib/searchEditor/browser/searchEditorActions'; import { getOrMakeSearchEditorInput, SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; //#region Editor Descriptior Registry.as(EditorExtensions.Editors).registerEditor( @@ -51,22 +51,18 @@ class SearchEditorContribution implements IWorkbenchContribution { @IInstantiationService protected readonly instantiationService: IInstantiationService, @ITelemetryService protected readonly telemetryService: ITelemetryService, @IContextKeyService protected readonly contextKeyService: IContextKeyService, - @IConfigurationService private readonly configurationService: IConfigurationService, ) { - const enableSearchEditorPreview = SearchEditorConstants.EnableSearchEditorPreview.bindTo(this.contextKeyService); - - enableSearchEditorPreview.set(this.searchConfig.enableSearchEditorPreview); - configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('search.previewSearchEditor')) { - enableSearchEditorPreview.set(this.searchConfig.enableSearchEditorPreview); - } - }); this.editorService.overrideOpenEditor((editor, options, group) => { - const resource = editor.resource; - if (!resource || - !(endsWith(resource.path, '.code-search') || resource.scheme === SearchEditorConstants.SearchEditorScheme) || - !(editor instanceof FileEditorInput || (resource.scheme === SearchEditorConstants.SearchEditorScheme))) { + let resource = editor.resource; + if (!resource) { return undefined; } + + if (resource.scheme === SearchEditorConstants.SearchEditorBodyScheme) { + resource = resource.with({ scheme: SearchEditorConstants.SearchEditorScheme }); + } + + if (resource.scheme !== SearchEditorConstants.SearchEditorScheme + && !(endsWith(resource.path, '.code-search') && editor instanceof FileEditorInput)) { return undefined; } @@ -83,10 +79,6 @@ class SearchEditorContribution implements IWorkbenchContribution { return { override: Promise.resolve(opened) }; }); } - - private get searchConfig(): ISearchConfigurationProperties { - return this.configurationService.getValue('search'); - } } const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); @@ -158,6 +150,32 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ primary: KeyMod.Alt | KeyCode.KEY_L, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_L } }); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: SearchEditorConstants.SelectAllSearchEditorMatchesCommandId, + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor), + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_L, + handler: selectAllSearchEditorMatchesCommand +}); + +CommandsRegistry.registerCommand( + SearchEditorConstants.RerunSearchEditorSearchCommandId, + (accessor: ServicesAccessor) => { + const activeControl = accessor.get(IEditorService).activeControl; + if (activeControl instanceof SearchEditor) { + activeControl.triggerSearch({ resetCursor: false }); + } + }); + +CommandsRegistry.registerCommand( + SearchEditorConstants.CleanSearchEditorStateCommandId, + (accessor: ServicesAccessor) => { + const activeControl = accessor.get(IEditorService).activeControl; + if (activeControl instanceof SearchEditor) { + activeControl.cleanState(); + } + }); //#endregion //#region Actions @@ -167,12 +185,10 @@ const category = localize('search', "Search Editor"); registry.registerWorkbenchAction( SyncActionDescriptor.create(OpenResultsInEditorAction, OpenResultsInEditorAction.ID, OpenResultsInEditorAction.LABEL, { mac: { primary: KeyMod.CtrlCmd | KeyCode.Enter } }, - ContextKeyExpr.and(SearchConstants.HasSearchResults, SearchConstants.SearchViewFocusedKey, SearchEditorConstants.EnableSearchEditorPreview)), - 'Search Editor: Open Results in Editor', category, - ContextKeyExpr.and(SearchEditorConstants.EnableSearchEditorPreview)); + ContextKeyExpr.and(SearchConstants.HasSearchResults, SearchConstants.SearchViewFocusedKey)), + 'Search Editor: Open Results in Editor', category); registry.registerWorkbenchAction( SyncActionDescriptor.create(OpenSearchEditorAction, OpenSearchEditorAction.ID, OpenSearchEditorAction.LABEL), - 'Search Editor: Open New Search Editor', category, - ContextKeyExpr.and(SearchEditorConstants.EnableSearchEditorPreview)); + 'Search Editor: Open New Search Editor', category); //#endregion diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index 66cdb94e6e0..8f26a359018 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -47,6 +47,8 @@ import { assertIsDefined } from 'vs/base/common/types'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { Position } from 'vs/editor/common/core/position'; +import { Selection } from 'vs/editor/common/core/selection'; const RESULT_LINE_REGEX = /^(\s+)(\d+)(:| )(\s+)(.*)$/; const FILE_LINE_REGEX = /^(\S.*):$/; @@ -77,6 +79,7 @@ export class SearchEditor extends BaseTextEditor { private searchHistoryDelayer: Delayer; private messageDisposables: IDisposable[] = []; private container: HTMLElement; + private searchModel: SearchModel; constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -107,6 +110,8 @@ export class SearchEditor extends BaseTextEditor { this.inputFocusContextKey = InputBoxFocusedKey.bindTo(scopedContextKeyService); this.searchOperation = this._register(new LongRunningOperation(progressService)); this.searchHistoryDelayer = new Delayer(2000); + + this.searchModel = this._register(this.instantiationService.createInstance(SearchModel)); } createEditor(parent: HTMLElement) { @@ -200,7 +205,7 @@ export class SearchEditor extends BaseTextEditor { this.searchResultEditor = super.getControl() as CodeEditorWidget; this.searchResultEditor.onMouseUp(e => { if (e.event.detail === 2) { - const behaviour = this.configurationService.getValue('search').searchEditorPreview.doubleClickBehaviour; + const behaviour = this.configurationService.getValue('search').searchEditor.doubleClickBehaviour; const position = e.target.position; if (position && behaviour !== 'selectWord') { const line = this.searchResultEditor.getModel()?.getLineContent(position.lineNumber) ?? ''; @@ -292,10 +297,50 @@ export class SearchEditor extends BaseTextEditor { this.toggleIncludesExcludes(); } + cleanState() { + this.getInput()?.setDirty(false); + } + private get searchConfig(): ISearchConfigurationProperties { return this.configurationService.getValue('search'); } + private iterateThroughMatches(reverse: boolean) { + const model = this.searchResultEditor.getModel(); + if (!model) { return; } + + const lastLine = model.getLineCount() ?? 1; + const lastColumn = model.getLineLength(lastLine); + + const fallbackStart = reverse ? new Position(lastLine, lastColumn) : new Position(1, 1); + + const currentPosition = this.searchResultEditor.getSelection()?.getStartPosition() ?? fallbackStart; + + const matchRanges = this.getInput()?.getMatchRanges(); + if (!matchRanges) { return; } + + const matchRange = (reverse ? findPrevRange : findNextRange)(matchRanges, currentPosition); + + this.searchResultEditor.setSelection(matchRange); + this.searchResultEditor.revealLineInCenterIfOutsideViewport(matchRange.startLineNumber); + this.searchResultEditor.focus(); + } + + focusNextResult() { + this.iterateThroughMatches(false); + } + + focusPreviousResult() { + this.iterateThroughMatches(true); + } + + focusAllResults() { + this.searchResultEditor + .setSelections((this.getInput()?.getMatchRanges() ?? []).map( + range => new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn))); + this.searchResultEditor.focus(); + } + async triggerSearch(_options?: { resetCursor?: boolean; delay?: number; }) { const options = { resetCursor: true, delay: 0, ..._options }; @@ -304,7 +349,7 @@ export class SearchEditor extends BaseTextEditor { await this.doRunSearch(); this.toggleRunAgainMessage(false); if (options.resetCursor) { - this.searchResultEditor.setSelection(new Range(1, 1, 1, 1)); + this.searchResultEditor.setPosition(new Position(1, 1)); this.searchResultEditor.setScrollPosition({ scrollTop: 0, scrollLeft: 0 }); } }, options.delay); @@ -326,6 +371,8 @@ export class SearchEditor extends BaseTextEditor { } private async doRunSearch() { + this.searchModel.cancelSearch(true); + const startInput = this.getInput(); this.searchHistoryDelayer.trigger(() => { @@ -372,30 +419,26 @@ export class SearchEditor extends BaseTextEditor { catch (err) { return; } - const searchModel = this.instantiationService.createInstance(SearchModel); + this.searchOperation.start(500); - await searchModel.search(query).finally(() => this.searchOperation.stop()); + await this.searchModel.search(query).finally(() => this.searchOperation.stop()); const input = this.getInput(); if (!input || input !== startInput || JSON.stringify(config) !== JSON.stringify(this.readConfigFromWidget())) { - - searchModel.dispose(); return; } const controller = ReferencesController.get(this.searchResultEditor); controller.closeWidget(false); const labelFormatter = (uri: URI): string => this.labelService.getUriLabel(uri, { relative: true }); - const results = serializeSearchResultForEditor(searchModel.searchResult, config.includes, config.excludes, config.contextLines, labelFormatter, false); + const results = serializeSearchResultForEditor(this.searchModel.searchResult, config.includes, config.excludes, config.contextLines, labelFormatter, false); const { header, body } = await input.getModels(); this.modelService.updateModel(body, results.text); header.setValue(serializeSearchConfiguration(config)); input.setDirty(input.resource.scheme !== 'search-editor'); input.setMatchRanges(results.matchRanges); - - searchModel.dispose(); } layout(dimension: DOM.Dimension) { @@ -522,3 +565,24 @@ registerThemingParticipant((theme, collector) => { }); export const searchEditorTextInputBorder = registerColor('searchEditor.textInputBorder', { dark: inputBorder, light: inputBorder, hc: inputBorder }, localize('textInputBoxBorder', "Search editor text input box border.")); + +function findNextRange(matchRanges: Range[], currentPosition: Position) { + for (const matchRange of matchRanges) { + if (Position.isBefore(currentPosition, matchRange.getStartPosition())) { + return matchRange; + } + } + return matchRanges[0]; +} + +function findPrevRange(matchRanges: Range[], currentPosition: Position) { + for (let i = matchRanges.length - 1; i >= 0; i--) { + const matchRange = matchRanges[i]; + if (Position.isBefore(matchRange.getStartPosition(), currentPosition)) { + { + return matchRange; + } + } + } + return matchRanges[matchRanges.length - 1]; +} diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts index 7f1b02bff84..094acf25fd3 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts @@ -54,6 +54,14 @@ export const toggleSearchEditorContextLinesCommand = (accessor: ServicesAccessor } }; +export const selectAllSearchEditorMatchesCommand = (accessor: ServicesAccessor) => { + const editorService = accessor.get(IEditorService); + const input = editorService.activeEditor; + if (input instanceof SearchEditorInput) { + (editorService.activeControl as SearchEditor).focusAllResults(); + } +}; + export class OpenSearchEditorAction extends Action { @@ -61,7 +69,6 @@ export class OpenSearchEditorAction extends Action { static readonly LABEL = localize('search.openNewEditor', "Open New Search Editor"); constructor(id: string, label: string, - @IConfigurationService private configurationService: IConfigurationService, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(id, label, 'codicon-new-file'); @@ -76,9 +83,7 @@ export class OpenSearchEditorAction extends Action { } async run() { - if (this.configurationService.getValue('search').enableSearchEditorPreview) { - await this.instantiationService.invokeFunction(openNewSearchEditor); - } + await this.instantiationService.invokeFunction(openNewSearchEditor); } } @@ -89,7 +94,6 @@ export class OpenResultsInEditorAction extends Action { constructor(id: string, label: string, @IViewsService private viewsService: IViewsService, - @IConfigurationService private configurationService: IConfigurationService, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(id, label, 'codicon-go-to-file'); @@ -106,7 +110,7 @@ export class OpenResultsInEditorAction extends Action { async run() { const searchView = getSearchView(this.viewsService); - if (searchView && this.configurationService.getValue('search').enableSearchEditorPreview) { + if (searchView) { await this.instantiationService.invokeFunction(createEditorFromSearchResult, searchView.searchResult, searchView.searchIncludePattern.getValue(), searchView.searchExcludePattern.getValue()); } } diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts index 389caf35844..44b819f8abc 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts @@ -18,7 +18,7 @@ import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { EditorInput, GroupIdentifier, IEditorInput, IRevertOptions, ISaveOptions, IMoveResult } from 'vs/workbench/common/editor'; -import { SearchEditorFindMatchClass, SearchEditorScheme } from 'vs/workbench/contrib/searchEditor/browser/constants'; +import { SearchEditorFindMatchClass, SearchEditorScheme, SearchEditorBodyScheme } from 'vs/workbench/contrib/searchEditor/browser/constants'; import { extractSearchQuery, serializeSearchConfiguration } from 'vs/workbench/contrib/searchEditor/browser/searchEditorSerialization'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -339,7 +339,7 @@ export const getOrMakeSearchEditorInput = ( } } - const contentsModelURI = uri.with({ scheme: 'search-editor-body' }); + const contentsModelURI = uri.with({ scheme: SearchEditorBodyScheme }); const headerModelURI = uri.with({ scheme: 'search-editor-header' }); const contentsModel = modelService.getModel(contentsModelURI) ?? modelService.createModel('', modeService.create('search-result'), contentsModelURI); const headerModel = modelService.getModel(headerModelURI) ?? modelService.createModel('', modeService.create('search-result'), headerModelURI); diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts index eca8524c25f..389faee3931 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts @@ -42,7 +42,7 @@ const matchToSearchResultFormat = (match: Match): { line: string, ranges: Range[ const rangeOnThisLine = ({ start, end }: { start?: number; end?: number; }) => new Range(1, (start ?? 1) + prefixOffset, 1, (end ?? sourceLine.length + 1) + prefixOffset); - const matchRange = match.range(); + const matchRange = match.rangeInPreview(); const matchIsSingleLine = matchRange.startLineNumber === matchRange.endLineNumber; let lineRange; @@ -211,9 +211,12 @@ export const serializeSearchResultForEditor = ? contentPatternToSearchResultHeader(searchResult.query, rawIncludePattern, rawExcludePattern, contextLines) : []; + const filecount = searchResult.fileCount() > 1 ? localize('numFiles', "{0} files", searchResult.fileCount()) : localize('oneFile', "1 file"); + const resultcount = searchResult.count() > 1 ? localize('numResults', "{0} results", searchResult.count()) : localize('oneResult', "1 result"); + const info = [ searchResult.count() - ? localize('resultCount', "{0} results in {1} files", searchResult.count(), searchResult.fileCount()) + ? `${filecount} - ${resultcount}` : localize('noResults', "No Results"), '']; diff --git a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts index f37af65df09..70d7065de59 100644 --- a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts +++ b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts @@ -8,7 +8,7 @@ import { SnippetCompletionProvider } from 'vs/workbench/contrib/snippets/browser import { Position } from 'vs/editor/common/core/position'; import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry'; import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl'; -import { TextModel } from 'vs/editor/common/model/textModel'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets.contribution'; import { Snippet, SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFile'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; @@ -67,7 +67,7 @@ suite('SnippetsService', function () { test('snippet completions - simple', function () { const provider = new SnippetCompletionProvider(modeService, snippetService); - const model = TextModel.createFromString('', undefined, modeService.getLanguageIdentifier('fooLang')); + const model = createTextModel('', undefined, modeService.getLanguageIdentifier('fooLang')); return provider.provideCompletionItems(model, new Position(1, 1), context)!.then(result => { assert.equal(result.incomplete, undefined); @@ -78,7 +78,7 @@ suite('SnippetsService', function () { test('snippet completions - with prefix', function () { const provider = new SnippetCompletionProvider(modeService, snippetService); - const model = TextModel.createFromString('bar', undefined, modeService.getLanguageIdentifier('fooLang')); + const model = createTextModel('bar', undefined, modeService.getLanguageIdentifier('fooLang')); return provider.provideCompletionItems(model, new Position(1, 4), context)!.then(result => { assert.equal(result.incomplete, undefined); @@ -113,7 +113,7 @@ suite('SnippetsService', function () { )]); const provider = new SnippetCompletionProvider(modeService, snippetService); - const model = TextModel.createFromString('bar-bar', undefined, modeService.getLanguageIdentifier('fooLang')); + const model = createTextModel('bar-bar', undefined, modeService.getLanguageIdentifier('fooLang')); await provider.provideCompletionItems(model, new Position(1, 3), context)!.then(result => { assert.equal(result.incomplete, undefined); @@ -174,19 +174,19 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(modeService, snippetService); - let model = TextModel.createFromString('\t { assert.equal(result.suggestions.length, 1); model.dispose(); - model = TextModel.createFromString('\t { assert.equal(result.suggestions.length, 1); assert.equal((result.suggestions[0].range as any).insert.startColumn, 2); model.dispose(); - model = TextModel.createFromString('a { assert.equal(result.suggestions.length, 1); @@ -209,7 +209,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(modeService, snippetService); - let model = TextModel.createFromString('\n\t\n>/head>', undefined, modeService.getLanguageIdentifier('fooLang')); + let model = createTextModel('\n\t\n>/head>', undefined, modeService.getLanguageIdentifier('fooLang')); return provider.provideCompletionItems(model, new Position(1, 1), context)!.then(result => { assert.equal(result.suggestions.length, 1); return provider.provideCompletionItems(model, new Position(2, 2), context)!; @@ -239,7 +239,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(modeService, snippetService); - let model = TextModel.createFromString('', undefined, modeService.getLanguageIdentifier('fooLang')); + let model = createTextModel('', undefined, modeService.getLanguageIdentifier('fooLang')); return provider.provideCompletionItems(model, new Position(1, 1), context)!.then(result => { assert.equal(result.suggestions.length, 2); let [first, second] = result.suggestions; @@ -266,7 +266,7 @@ suite('SnippetsService', function () { )]); const provider = new SnippetCompletionProvider(modeService, snippetService); - let model = TextModel.createFromString('p-', undefined, modeService.getLanguageIdentifier('fooLang')); + let model = createTextModel('p-', undefined, modeService.getLanguageIdentifier('fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 2), context)!; assert.equal(result.suggestions.length, 1); @@ -291,7 +291,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(modeService, snippetService); - let model = TextModel.createFromString('Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea b', undefined, modeService.getLanguageIdentifier('fooLang')); + let model = createTextModel('Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea b', undefined, modeService.getLanguageIdentifier('fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 158), context)!; assert.equal(result.suggestions.length, 1); @@ -310,7 +310,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(modeService, snippetService); - let model = TextModel.createFromString(':', undefined, modeService.getLanguageIdentifier('fooLang')); + let model = createTextModel(':', undefined, modeService.getLanguageIdentifier('fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 2), context)!; assert.equal(result.suggestions.length, 0); @@ -329,7 +329,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(modeService, snippetService); - let model = TextModel.createFromString('template', undefined, modeService.getLanguageIdentifier('fooLang')); + let model = createTextModel('template', undefined, modeService.getLanguageIdentifier('fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 9), context)!; assert.equal(result.suggestions.length, 1); @@ -352,7 +352,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(modeService, snippetService); - let model = TextModel.createFromString('Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea b text_after_b', undefined, modeService.getLanguageIdentifier('fooLang')); + let model = createTextModel('Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea Thisisaverylonglinegoingwithmore100bcharactersandthismakesintellisensebecomea b text_after_b', undefined, modeService.getLanguageIdentifier('fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 158), context)!; assert.equal(result.suggestions.length, 1); @@ -374,7 +374,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(modeService, snippetService); - let model = TextModel.createFromString('.🐷-a-b', undefined, modeService.getLanguageIdentifier('fooLang')); + let model = createTextModel('.🐷-a-b', undefined, modeService.getLanguageIdentifier('fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 8), context)!; assert.equal(result.suggestions.length, 1); @@ -395,7 +395,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(modeService, snippetService); - let model = TextModel.createFromString('a ', undefined, modeService.getLanguageIdentifier('fooLang')); + let model = createTextModel('a ', undefined, modeService.getLanguageIdentifier('fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 3), context)!; assert.equal(result.suggestions.length, 1); @@ -422,14 +422,14 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(modeService, snippetService); - let model = TextModel.createFromString(' <', undefined, modeService.getLanguageIdentifier('fooLang')); + let model = createTextModel(' <', undefined, modeService.getLanguageIdentifier('fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 3), context)!; assert.equal(result.suggestions.length, 1); let [first] = result.suggestions; assert.equal((first.range as any).insert.startColumn, 2); - model = TextModel.createFromString('1', undefined, modeService.getLanguageIdentifier('fooLang')); + model = createTextModel('1', undefined, modeService.getLanguageIdentifier('fooLang')); result = await provider.provideCompletionItems(model, new Position(1, 2), context)!; assert.equal(result.suggestions.length, 1); @@ -450,7 +450,7 @@ suite('SnippetsService', function () { const provider = new SnippetCompletionProvider(modeService, snippetService); - let model = TextModel.createFromString('not wordFoo bar', undefined, modeService.getLanguageIdentifier('fooLang')); + let model = createTextModel('not wordFoo bar', undefined, modeService.getLanguageIdentifier('fooLang')); let result = await provider.provideCompletionItems(model, new Position(1, 3), context)!; assert.equal(result.suggestions.length, 1); @@ -458,7 +458,7 @@ suite('SnippetsService', function () { assert.equal((first.range as any).insert.endColumn, 3); assert.equal((first.range as any).replace.endColumn, 9); - model = TextModel.createFromString('not woFoo bar', undefined, modeService.getLanguageIdentifier('fooLang')); + model = createTextModel('not woFoo bar', undefined, modeService.getLanguageIdentifier('fooLang')); result = await provider.provideCompletionItems(model, new Position(1, 3), context)!; assert.equal(result.suggestions.length, 1); @@ -466,7 +466,7 @@ suite('SnippetsService', function () { assert.equal((first.range as any).insert.endColumn, 3); assert.equal((first.range as any).replace.endColumn, 3); - model = TextModel.createFromString('not word', undefined, modeService.getLanguageIdentifier('fooLang')); + model = createTextModel('not word', undefined, modeService.getLanguageIdentifier('fooLang')); result = await provider.provideCompletionItems(model, new Position(1, 1), context)!; assert.equal(result.suggestions.length, 1); diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 09ba6cc1fa9..27418989dde 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -57,13 +57,13 @@ import { TaskSorter, TaskIdentifier, KeyedTaskIdentifier, TASK_RUNNING_STATE, TaskRunSource, KeyedTaskIdentifier as NKeyedTaskIdentifier, TaskDefinition } from 'vs/workbench/contrib/tasks/common/tasks'; -import { ITaskService, ITaskProvider, ProblemMatcherRunOptions, CustomizationProperties, TaskFilter, WorkspaceFolderTaskResult } from 'vs/workbench/contrib/tasks/common/taskService'; +import { ITaskService, ITaskProvider, ProblemMatcherRunOptions, CustomizationProperties, TaskFilter, WorkspaceFolderTaskResult, USER_TASKS_GROUP_KEY } from 'vs/workbench/contrib/tasks/common/taskService'; import { getTemplates as getTaskTemplates } from 'vs/workbench/contrib/tasks/common/taskTemplates'; import * as TaskConfig from '../common/taskConfiguration'; import { TerminalTaskSystem } from './terminalTaskSystem'; -import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickInputService, IQuickPickItem, QuickPickInput, IQuickPick } from 'vs/platform/quickinput/common/quickInput'; import { TaskDefinitionRegistry } from 'vs/workbench/contrib/tasks/common/taskDefinitionRegistry'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -80,14 +80,13 @@ import { IPreferencesService } from 'vs/workbench/services/preferences/common/pr import { find } from 'vs/base/common/arrays'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { IViewsService } from 'vs/workbench/common/views'; +import { ProviderProgressMananger } from 'vs/workbench/contrib/tasks/browser/providerProgressManager'; const QUICKOPEN_HISTORY_LIMIT_CONFIG = 'task.quickOpen.history'; const QUICKOPEN_DETAIL_CONFIG = 'task.quickOpen.detail'; const PROBLEM_MATCHER_NEVER_CONFIG = 'task.problemMatchers.neverPrompt'; const QUICKOPEN_SKIP_CONFIG = 'task.quickOpen.skip'; -const SETTINGS_GROUP_KEY = 'settings'; - export namespace ConfigureTaskAction { export const ID = 'workbench.action.tasks.configureTaskRunner'; export const TEXT = nls.localize('ConfigureTaskRunnerAction.label', "Configure Task"); @@ -220,6 +219,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer private _providers: Map; private _providerTypes: Map; protected _taskSystemInfos: Map; + private _providerProgressManager: ProviderProgressMananger | undefined; protected _workspaceTasksPromise?: Promise>; protected _areJsonTasksSupportedPromise: Promise = Promise.resolve(false); @@ -551,7 +551,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } return this.getGroupedTasks().then((map) => { let values = map.get(folder); - values = values.concat(map.get(SETTINGS_GROUP_KEY)); + values = values.concat(map.get(USER_TASKS_GROUP_KEY)); if (!values) { return undefined; @@ -562,6 +562,37 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer protected abstract versionAndEngineCompatible(filter?: TaskFilter): boolean; + private tasksAndGroupedTasks(filter?: TaskFilter): { tasks: Promise, grouped: Promise } { + if (!this.versionAndEngineCompatible(filter)) { + return { tasks: Promise.resolve([]), grouped: Promise.resolve(new TaskMap()) }; + } + const grouped = this.getGroupedTasks(filter ? filter.type : undefined); + const tasks = grouped.then((map) => { + if (!filter || !filter.type) { + return map.all(); + } + let result: Task[] = []; + map.forEach((tasks) => { + for (let task of tasks) { + if (ContributedTask.is(task) && task.defines.type === filter.type) { + result.push(task); + } else if (CustomTask.is(task)) { + if (task.type === filter.type) { + result.push(task); + } else { + let customizes = task.customizes(); + if (customizes && customizes.type === filter.type) { + result.push(task); + } + } + } + } + }); + return result; + }); + return { tasks, grouped }; + } + public tasks(filter?: TaskFilter): Promise { if (!this.versionAndEngineCompatible(filter)) { return Promise.resolve([]); @@ -705,11 +736,11 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }); } - public run(task: Task | undefined, options?: ProblemMatcherRunOptions, runSource: TaskRunSource = TaskRunSource.System): Promise { + public run(task: Task | undefined, options?: ProblemMatcherRunOptions, runSource: TaskRunSource = TaskRunSource.System, grouped?: Promise): Promise { if (!task) { throw new TaskError(Severity.Info, nls.localize('TaskServer.noTask', 'Task to execute is undefined'), TaskErrors.TaskNotFound); } - return this.getGroupedTasks().then((grouped) => { + return (grouped ?? this.getGroupedTasks()).then((grouped) => { let resolver = this.createResolver(grouped); if (options && options.attachProblemMatcher && this.shouldAttachProblemMatcher(task) && !InMemoryTask.is(task)) { return this.attachProblemMatcher(task).then((toExecute) => { @@ -1169,8 +1200,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } }); let resolver: ITaskResolver = { - resolve: (uri: URI, alias: string) => { - let data = resolverData.get(uri.toString()); + resolve: (uri: URI | string, alias: string) => { + let data = resolverData.get(typeof uri === 'string' ? uri : uri.toString()); if (!data) { return undefined; } @@ -1236,8 +1267,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }); return { - resolve: (uri: URI, identifier: string | TaskIdentifier | undefined) => { - let data = uri ? resolverData.get(uri.toString()) : undefined; + resolve: (uri: URI | string, identifier: string | TaskIdentifier | undefined) => { + let data = resolverData.get(typeof uri === 'string' ? uri : uri.toString()); if (!data || !identifier) { return undefined; } @@ -1347,11 +1378,24 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer protected abstract getTaskSystem(): ITaskSystem; - private async provideTasksWithWarning(provider: ITaskProvider, type: string, validTypes: IStringDictionary): Promise { + private async provideTasksWithWarning(provider: ITaskProvider, type: string, validTypes: IStringDictionary): Promise { return new Promise(async (resolve, reject) => { - provider.provideTasks(validTypes).then((value) => { + let isDone = false; + let disposable: IDisposable | undefined; + const providePromise = provider.provideTasks(validTypes); + this._providerProgressManager?.addProvider(type, providePromise); + disposable = this._providerProgressManager?.canceled.token.onCancellationRequested(() => { + if (!isDone) { + resolve(); + } + }); + providePromise.then((value) => { + isDone = true; + disposable?.dispose(); resolve(value); }, (e) => { + isDone = true; + disposable?.dispose(); reject(e); }); }); @@ -1363,10 +1407,11 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer TaskDefinitionRegistry.all().forEach(definition => validTypes[definition.taskType] = true); validTypes['shell'] = true; validTypes['process'] = true; + this._providerProgressManager = new ProviderProgressMananger(); return new Promise(resolve => { let result: TaskSet[] = []; let counter: number = 0; - let done = (value: TaskSet) => { + let done = (value: TaskSet | undefined) => { if (value) { result.push(value); } @@ -1584,7 +1629,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } const userTasks = await this.computeUserTasks(this.workspaceFolders[0], runSource).then((value) => value, () => undefined); if (userTasks) { - result.set(SETTINGS_GROUP_KEY, userTasks); + result.set(USER_TASKS_GROUP_KEY, userTasks); } const workspaceFileTasks = await this.computeWorkspaceFileTasks(this.workspaceFolders[0], runSource).then((value) => value, () => undefined); if (workspaceFileTasks && this._workspace && this._workspace.configuration) { @@ -2062,30 +2107,69 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } return entries; }); - return this.quickInputService.pick(pickEntries, { - placeHolder, - matchOnDescription: true, - onDidTriggerItemButton: context => { - let task = context.item.task; - this.quickInputService.cancel(); - if (ContributedTask.is(task)) { - this.customize(task, undefined, true); - } else if (CustomTask.is(task)) { - this.openConfig(task); + + const picker: IQuickPick = this.quickInputService.createQuickPick(); + picker.placeholder = placeHolder; + picker.matchOnDescription = true; + picker.ignoreFocusOut = true; + + picker.onDidTriggerItemButton(context => { + let task = context.item.task; + this.quickInputService.cancel(); + if (ContributedTask.is(task)) { + this.customize(task, undefined, true); + } else if (CustomTask.is(task)) { + this.openConfig(task); + } + }); + picker.busy = true; + const progressManager = this._providerProgressManager; + const progressTimeout = setTimeout(() => { + if (progressManager) { + progressManager.showProgress = (stillProviding, total) => { + let message = undefined; + if (stillProviding.length > 0) { + message = nls.localize('pickProgressManager.description', 'Detecting tasks ({0} of {1}): {2} in progress', total - stillProviding.length, total, stillProviding.join(', ')); + } + picker.description = message; + }; + progressManager.addOnDoneListener(() => { + picker.focusOnInput(); + picker.customButton = false; + }); + if (!progressManager.isDone) { + picker.customLabel = nls.localize('taskQuickPick.cancel', "Stop detecting"); + picker.onDidCustom(() => { + this._providerProgressManager?.cancel(); + }); + picker.customButton = true; } } - }, cancellationToken).then(async (selection) => { - if (cancellationToken.isCancellationRequested) { - // canceled when there's only one task - const task = (await pickEntries)[0]; - if ((task).task) { - selection = task; + }, 1000); + pickEntries.then(entries => { + clearTimeout(progressTimeout); + progressManager?.dispose(); + picker.busy = false; + picker.items = entries; + }); + picker.show(); + + return new Promise(resolve => { + this._register(picker.onDidAccept(async () => { + let selection = picker.selectedItems ? picker.selectedItems[0] : undefined; + if (cancellationToken.isCancellationRequested) { + // canceled when there's only one task + const task = (await pickEntries)[0]; + if ((task).task) { + selection = task; + } } - } - if (!selection) { - return; - } - return selection; + picker.dispose(); + if (!selection) { + resolve(); + } + resolve(selection); + })); }); } @@ -2118,9 +2202,10 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (identifier !== undefined) { this.getGroupedTasks().then((grouped) => { let resolver = this.createResolver(grouped); - let folders = this.contextService.getWorkspace().folders; + let folders: (IWorkspaceFolder | string)[] = this.contextService.getWorkspace().folders; + folders = folders.concat([USER_TASKS_GROUP_KEY]); for (let folder of folders) { - let task = resolver.resolve(folder.uri, identifier); + let task = resolver.resolve(typeof folder === 'string' ? folder : folder.uri, identifier); if (task) { this.run(task).then(undefined, reason => { // eat the error, it has already been surfaced to the user and we don't care about it here @@ -2139,7 +2224,11 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer private doRunTaskCommand(tasks?: Task[]): void { this.showIgnoredFoldersMessage().then(() => { - this.showQuickPick(tasks ? tasks : this.tasks(), + let taskResult: { tasks: Promise, grouped: Promise } | undefined = undefined; + if (!tasks) { + taskResult = this.tasksAndGroupedTasks(); + } + this.showQuickPick(tasks ? tasks : taskResult!.tasks, nls.localize('TaskService.pickRunTask', 'Select the task to run'), { label: nls.localize('TaskService.noEntryToRun', 'No task to run found. Configure Tasks...'), @@ -2154,7 +2243,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (task === null) { this.runConfigureTasks(); } else { - this.run(task, { attachProblemMatcher: true }, TaskRunSource.User).then(undefined, reason => { + this.run(task, { attachProblemMatcher: true }, TaskRunSource.User, taskResult?.grouped).then(undefined, reason => { // eat the error, it has already been surfaced to the user and we don't care about it here }); } diff --git a/src/vs/workbench/contrib/tasks/browser/providerProgressManager.ts b/src/vs/workbench/contrib/tasks/browser/providerProgressManager.ts new file mode 100644 index 00000000000..7c48da7f73c --- /dev/null +++ b/src/vs/workbench/contrib/tasks/browser/providerProgressManager.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TaskSet } from 'vs/workbench/contrib/tasks/common/tasks'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; + +export class ProviderProgressMananger extends Disposable { + private _onProviderComplete: Emitter = new Emitter(); + private _stillProviding: Set = new Set(); + private _totalProviders: number = 0; + private _onDone: Emitter = new Emitter(); + private _isDone: boolean = false; + private _showProgress: ((remaining: string[], total: number) => void) | undefined; + public canceled: CancellationTokenSource = new CancellationTokenSource(); + + constructor() { + super(); + this._register(this._onProviderComplete.event(taskType => { + this._stillProviding.delete(taskType); + if (this._stillProviding.size === 0) { + this._isDone = true; + this._onDone.fire(); + } + if (this._showProgress) { + this._showProgress(Array.from(this._stillProviding), this._totalProviders); + } + })); + } + + public addProvider(taskType: string, provider: Promise) { + this._totalProviders++; + this._stillProviding.add(taskType); + provider.then(() => this._onProviderComplete.fire(taskType)); + } + + public addOnDoneListener(onDoneListener: () => void) { + this._register(this._onDone.event(onDoneListener)); + } + + set showProgress(progressDisplayFunction: (remaining: string[], total: number) => void) { + this._showProgress = progressDisplayFunction; + this._showProgress(Array.from(this._stillProviding), this._totalProviders); + } + + get isDone(): boolean { + return this._isDone; + } + + public cancel() { + this._isDone = true; + if (this._showProgress) { + this._showProgress([], 0); + } + this._onDone.fire(); + this.canceled.cancel(); + } +} diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index ae20fb999fe..bf6617e3b90 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -438,6 +438,7 @@ export class TerminalTaskSystem implements ITaskSystem { this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.DependsOnStarted, task)); promise = this.executeTask(dependencyTask, resolver, trigger, alreadyResolved); } + promises.push(promise); if (task.configurationProperties.dependsOrder === DependsOrder.sequence) { const promiseResult = await promise; if (promiseResult.exitCode === 0) { diff --git a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts index 5e6b05391ab..14666552147 100644 --- a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts +++ b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts @@ -22,6 +22,8 @@ import { IWorkspaceFolder, IWorkspace } from 'vs/platform/workspace/common/works import * as Tasks from './tasks'; import { TaskDefinitionRegistry } from './taskDefinitionRegistry'; import { ConfiguredInput } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; +import { URI } from 'vs/base/common/uri'; +import { USER_TASKS_GROUP_KEY } from 'vs/workbench/contrib/tasks/common/taskService'; export const enum ShellQuoting { @@ -679,6 +681,7 @@ export namespace RunOnOptions { } export namespace RunOptions { + const properties: MetaData[] = [{ property: 'reevaluateOnRerun' }, { property: 'runOn' }, { property: 'instanceLimit' }]; export function fromConfiguration(value: RunOptionsConfig | undefined): Tasks.RunOptions { return { reevaluateOnRerun: value ? value.reevaluateOnRerun : true, @@ -686,6 +689,14 @@ export namespace RunOptions { instanceLimit: value ? value.instanceLimit : 1 }; } + + export function assignProperties(target: Tasks.RunOptions, source: Tasks.RunOptions | undefined): Tasks.RunOptions { + return _assignProperties(target, source, properties)!; + } + + export function fillProperties(target: Tasks.RunOptions, source: Tasks.RunOptions | undefined): Tasks.RunOptions { + return _fillProperties(target, source, properties)!; + } } interface ParseContext { @@ -1231,9 +1242,15 @@ namespace GroupKind { } namespace TaskDependency { - export function from(this: void, external: string | TaskIdentifier, context: ParseContext): Tasks.TaskDependency | undefined { + export function from(this: void, external: string | TaskIdentifier, context: ParseContext, source: TaskConfigSource): Tasks.TaskDependency | undefined { if (Types.isString(external)) { - return { uri: context.workspace && context.workspace.configuration ? context.workspace.configuration : context.workspaceFolder.uri, task: external }; + let uri: URI | string; + if (source === TaskConfigSource.User) { + uri = USER_TASKS_GROUP_KEY; + } else { + uri = context.workspace && context.workspace.configuration ? context.workspace.configuration : context.workspaceFolder.uri; + } + return { uri, task: external }; } else if (TaskIdentifier.is(external)) { return { uri: context.workspace && context.workspace.configuration ? context.workspace.configuration : context.workspaceFolder.uri, @@ -1267,7 +1284,7 @@ namespace ConfigurationProperties { { property: 'options' } ]; - export function from(this: void, external: ConfigurationProperties & { [key: string]: any; }, context: ParseContext, includeCommandOptions: boolean, properties?: IJSONSchemaMap): Tasks.ConfigurationProperties | undefined { + export function from(this: void, external: ConfigurationProperties & { [key: string]: any; }, context: ParseContext, includeCommandOptions: boolean, source: TaskConfigSource, properties?: IJSONSchemaMap): Tasks.ConfigurationProperties | undefined { if (!external) { return undefined; } @@ -1311,14 +1328,14 @@ namespace ConfigurationProperties { if (external.dependsOn !== undefined) { if (Types.isArray(external.dependsOn)) { result.dependsOn = external.dependsOn.reduce((dependencies: Tasks.TaskDependency[], item): Tasks.TaskDependency[] => { - const dependency = TaskDependency.from(item, context); + const dependency = TaskDependency.from(item, context, source); if (dependency) { dependencies.push(dependency); } return dependencies; }, []); } else { - const dependsOnValue = TaskDependency.from(external.dependsOn, context); + const dependsOnValue = TaskDependency.from(external.dependsOn, context, source); result.dependsOn = dependsOnValue ? [dependsOnValue] : undefined; } } @@ -1435,7 +1452,7 @@ namespace ConfiguringTask { RunOptions.fromConfiguration(external.runOptions), {} ); - let configuration = ConfigurationProperties.from(external, context, true, typeDeclaration.properties); + let configuration = ConfigurationProperties.from(external, context, true, source, typeDeclaration.properties); if (configuration) { result.configurationProperties = Objects.assign(result.configurationProperties, configuration); if (result.configurationProperties.name) { @@ -1512,7 +1529,7 @@ namespace CustomTask { identifier: taskName, } ); - let configuration = ConfigurationProperties.from(external, context, false); + let configuration = ConfigurationProperties.from(external, context, false, source); if (configuration) { result.configurationProperties = Objects.assign(result.configurationProperties, configuration); } @@ -1601,6 +1618,7 @@ namespace CustomTask { result.command.presentation = CommandConfiguration.PresentationOptions.assignProperties( result.command.presentation!, configuredProps.configurationProperties.presentation)!; result.command.options = CommandOptions.assignProperties(result.command.options, configuredProps.configurationProperties.options); + result.runOptions = RunOptions.assignProperties(result.runOptions, configuredProps.runOptions); let contributedConfigProps: Tasks.ConfigurationProperties = contributedTask.configurationProperties; fillProperty(resultConfigProps, contributedConfigProps, 'group'); @@ -1613,6 +1631,7 @@ namespace CustomTask { result.command.presentation = CommandConfiguration.PresentationOptions.fillProperties( result.command.presentation!, contributedConfigProps.presentation)!; result.command.options = CommandOptions.fillProperties(result.command.options, contributedConfigProps.options); + result.runOptions = RunOptions.fillProperties(result.runOptions, contributedTask.runOptions); if (contributedTask.hasDefinedMatchers === true) { result.hasDefinedMatchers = true; diff --git a/src/vs/workbench/contrib/tasks/common/taskService.ts b/src/vs/workbench/contrib/tasks/common/taskService.ts index fb2167017af..2342fedf370 100644 --- a/src/vs/workbench/contrib/tasks/common/taskService.ts +++ b/src/vs/workbench/contrib/tasks/common/taskService.ts @@ -50,6 +50,8 @@ export interface WorkspaceFolderTaskResult extends WorkspaceTaskResult { workspaceFolder: IWorkspaceFolder; } +export const USER_TASKS_GROUP_KEY = 'settings'; + export interface ITaskService { _serviceBrand: undefined; onDidStateChange: Event; diff --git a/src/vs/workbench/contrib/tasks/common/taskSystem.ts b/src/vs/workbench/contrib/tasks/common/taskSystem.ts index 888d79b912c..0ac0d5799a6 100644 --- a/src/vs/workbench/contrib/tasks/common/taskSystem.ts +++ b/src/vs/workbench/contrib/tasks/common/taskSystem.ts @@ -93,7 +93,7 @@ export interface ITaskExecuteResult { } export interface ITaskResolver { - resolve(uri: URI, identifier: string | KeyedTaskIdentifier | undefined): Task | undefined; + resolve(uri: URI | string, identifier: string | KeyedTaskIdentifier | undefined): Task | undefined; } export interface TaskTerminateResponse extends TerminateResponse { diff --git a/src/vs/workbench/contrib/tasks/common/tasks.ts b/src/vs/workbench/contrib/tasks/common/tasks.ts index 8130ab56092..2fcd3f90195 100644 --- a/src/vs/workbench/contrib/tasks/common/tasks.ts +++ b/src/vs/workbench/contrib/tasks/common/tasks.ts @@ -438,7 +438,7 @@ export interface KeyedTaskIdentifier extends TaskIdentifier { } export interface TaskDependency { - uri: URI; + uri: URI | string; task: string | KeyedTaskIdentifier | undefined; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index cec072d72b3..a391706e51f 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -602,8 +602,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Respect chords if the allowChords setting is set and it's not Escape. Escape is // handled specially for Zen Mode's Escape, Escape chord, plus it's important in // terminals generally - const allowChords = resolveResult && resolveResult.enterChord && this._configHelper.config.allowChords && event.key !== 'Escape'; - if (allowChords || resolveResult && this._skipTerminalCommands.some(k => k === resolveResult.commandId)) { + const allowChords = resolveResult?.enterChord && this._configHelper.config.allowChords && event.key !== 'Escape'; + if (this._keybindingService.inChordMode || allowChords || resolveResult && this._skipTerminalCommands.some(k => k === resolveResult.commandId)) { event.preventDefault(); return false; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index a45eaa00771..12bc4fab7ff 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -57,7 +57,7 @@ export class TerminalViewPane extends ViewPane { @IStorageService storageService: IStorageService, @IOpenerService openerService: IOpenerService, ) { - super(options, keybindingService, _contextMenuService, configurationService, contextKeyService, viewDescriptorService, _instantiationService, openerService, themeService); + super(options, keybindingService, _contextMenuService, configurationService, contextKeyService, viewDescriptorService, _instantiationService, openerService, themeService, telemetryService); } protected renderBody(container: HTMLElement): void { @@ -114,7 +114,7 @@ export class TerminalViewPane extends ViewPane { })); // Force another layout (first is setContainers) since config has changed - this.layoutBody(this._terminalContainer.offsetWidth, this._terminalContainer.offsetHeight); + this.layoutBody(this._terminalContainer.offsetHeight, this._terminalContainer.offsetWidth); } protected layoutBody(height: number, width: number): void { @@ -321,7 +321,7 @@ export class TerminalViewPane extends ViewPane { } // TODO: Can we support ligatures? // dom.toggleClass(this._parentDomElement, 'enable-ligatures', this._terminalService.configHelper.config.fontLigatures); - this.layoutBody(this._parentDomElement.offsetWidth, this._parentDomElement.offsetHeight); + this.layoutBody(this._parentDomElement.offsetHeight, this._parentDomElement.offsetWidth); } } diff --git a/src/vs/workbench/contrib/timeline/browser/media/timelinePane.css b/src/vs/workbench/contrib/timeline/browser/media/timelinePane.css index ad6200c6561..3b036147cfc 100644 --- a/src/vs/workbench/contrib/timeline/browser/media/timelinePane.css +++ b/src/vs/workbench/contrib/timeline/browser/media/timelinePane.css @@ -12,6 +12,7 @@ opacity: 0.5; position: absolute; pointer-events: none; + z-index: 1; } .timeline-tree-view .monaco-list .monaco-list-row .custom-view-tree-node-item .monaco-icon-label { diff --git a/src/vs/workbench/contrib/timeline/browser/timeline.contribution.ts b/src/vs/workbench/contrib/timeline/browser/timeline.contribution.ts index 142fb5cb596..b3958cb47c7 100644 --- a/src/vs/workbench/contrib/timeline/browser/timeline.contribution.ts +++ b/src/vs/workbench/contrib/timeline/browser/timeline.contribution.ts @@ -14,6 +14,8 @@ import { TimelineService } from 'vs/workbench/contrib/timeline/common/timelineSe import { TimelinePane } from './timelinePane'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { ICommandHandler, CommandsRegistry } from 'vs/platform/commands/common/commands'; import product from 'vs/platform/product/common/product'; export class TimelinePaneDescriptor implements IViewDescriptor { @@ -42,13 +44,102 @@ configurationRegistry.registerConfiguration({ 'timeline.showView': { type: 'boolean', description: localize('timeline.showView', "Experimental: When enabled, shows a Timeline view in the Explorer sidebar."), - default: false //product.quality !== 'stable' + default: product.quality !== 'stable' + }, + 'timeline.excludeSources': { + type: 'array', + description: localize('timeline.excludeSources', "Experimental: An array of Timeline sources that should be excluded from the Timeline view"), + default: null }, } }); -if (product.quality !== 'stable') { - Registry.as(ViewExtensions.ViewsRegistry).registerViews([new TimelinePaneDescriptor()], VIEW_CONTAINER); +Registry.as(ViewExtensions.ViewsRegistry).registerViews([new TimelinePaneDescriptor()], VIEW_CONTAINER); + +namespace TimelineViewRefreshAction { + + export const ID = 'timeline.refresh'; + export const LABEL = localize('timeline.refreshView', "Refresh"); + + export function handler(): ICommandHandler { + return (accessor, arg) => { + const service = accessor.get(ITimelineService); + return service.reset(); + }; + } } +CommandsRegistry.registerCommand(TimelineViewRefreshAction.ID, TimelineViewRefreshAction.handler()); + +// namespace TimelineViewRefreshHardAction { + +// export const ID = 'timeline.refreshHard'; +// export const LABEL = localize('timeline.refreshHard', "Refresh (Hard)"); + +// export function handler(fetch?: 'all' | 'more'): ICommandHandler { +// return (accessor, arg) => { +// const service = accessor.get(ITimelineService); +// return service.refresh(fetch); +// }; +// } +// } + +// CommandsRegistry.registerCommand(TimelineViewRefreshAction.ID, TimelineViewRefreshAction.handler()); + +// namespace TimelineViewLoadMoreAction { + +// export const ID = 'timeline.loadMore'; +// export const LABEL = localize('timeline.loadMoreInView', "Load More"); + +// export function handler(): ICommandHandler { +// return (accessor, arg) => { +// const service = accessor.get(ITimelineService); +// return service.refresh('more'); +// }; +// } +// } + +// CommandsRegistry.registerCommand(TimelineViewLoadMoreAction.ID, TimelineViewLoadMoreAction.handler()); + +// namespace TimelineViewLoadAllAction { + +// export const ID = 'timeline.loadAll'; +// export const LABEL = localize('timeline.loadAllInView', "Load All"); + +// export function handler(): ICommandHandler { +// return (accessor, arg) => { +// const service = accessor.get(ITimelineService); +// return service.refresh('all'); +// }; +// } +// } + +// CommandsRegistry.registerCommand(TimelineViewLoadAllAction.ID, TimelineViewLoadAllAction.handler()); + +MenuRegistry.appendMenuItem(MenuId.TimelineTitle, ({ + group: 'navigation', + order: 1, + command: { + id: TimelineViewRefreshAction.ID, + title: TimelineViewRefreshAction.LABEL, + icon: { id: 'codicon/refresh' } + } +})); + +// MenuRegistry.appendMenuItem(MenuId.TimelineTitle, ({ +// group: 'navigation', +// order: 2, +// command: { +// id: TimelineViewLoadMoreAction.ID, +// title: TimelineViewLoadMoreAction.LABEL, +// icon: { id: 'codicon/unfold' } +// }, +// alt: { +// id: TimelineViewLoadAllAction.ID, +// title: TimelineViewLoadAllAction.LABEL, +// icon: { id: 'codicon/unfold' } + +// } +// })); + registerSingleton(ITimelineService, TimelineService, true); diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index 1afbedc6563..d438a1b739c 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -8,6 +8,7 @@ import { localize } from 'vs/nls'; import * as DOM from 'vs/base/browser/dom'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; +import { Iterator } from 'vs/base/common/iterator'; import { DisposableStore, IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; @@ -18,9 +19,9 @@ import { TreeResourceNavigator, WorkbenchObjectTree } from 'vs/platform/list/bro import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ITimelineService, TimelineChangeEvent, TimelineProvidersChangeEvent, TimelineRequest, TimelineItem } from 'vs/workbench/contrib/timeline/common/timeline'; +import { ITimelineService, TimelineChangeEvent, TimelineItem, TimelineOptions, TimelineProvidersChangeEvent, TimelineRequest, Timeline } from 'vs/workbench/contrib/timeline/common/timeline'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { SideBySideEditor, toResource } from 'vs/workbench/common/editor'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -28,7 +29,6 @@ import { IThemeService, LIGHT, ThemeIcon } from 'vs/platform/theme/common/themeS import { IViewDescriptorService } from 'vs/workbench/common/views'; import { basename } from 'vs/base/common/path'; import { IProgressService } from 'vs/platform/progress/common/progress'; -import { VIEWLET_ID } from 'vs/workbench/contrib/files/common/files'; import { debounce } from 'vs/base/common/decorators'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IActionViewItemProvider, ActionBar, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -36,16 +36,55 @@ import { IAction, ActionRunner } from 'vs/base/common/actions'; import { ContextAwareMenuEntryActionViewItem, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { MenuItemAction, IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { fromNow } from 'vs/base/common/date'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -// TODO[ECA]: Localize all the strings +const InitialPageSize = 20; +const SubsequentPageSize = 40; + +interface CommandItem { + handle: 'vscode-command:loadMore'; + timestamp: number; + label: string; + themeIcon?: { id: string }; + description?: string; + detail?: string; + contextValue?: string; + + // Make things easier for duck typing + id: undefined; + icon: undefined; + iconDark: undefined; + source: undefined; +} + +type TreeElement = TimelineItem | CommandItem; + +// function isCommandItem(item: TreeElement | undefined): item is CommandItem { +// return item?.handle.startsWith('vscode-command:') ?? false; +// } + +function isLoadMoreCommandItem(item: TreeElement | undefined): item is CommandItem & { + handle: 'vscode-command:loadMore'; +} { + return item?.handle === 'vscode-command:loadMore'; +} + +function isTimelineItem(item: TreeElement | undefined): item is TimelineItem { + return !item?.handle.startsWith('vscode-command:') ?? false; +} -type TreeElement = TimelineItem; interface TimelineActionContext { uri: URI | undefined; item: TreeElement; } +interface TimelineCursors { + startCursors?: { before: any; after?: any }; + endCursors?: { before: any; after?: any }; + more: boolean; +} + export class TimelinePane extends ViewPane { static readonly ID = 'timeline'; static readonly TITLE = localize('timeline', 'Timeline'); @@ -58,8 +97,9 @@ export class TimelinePane extends ViewPane { private _menus: TimelineMenus; private _visibilityDisposables: DisposableStore | undefined; - // private _excludedSources: Set | undefined; - private _items: TimelineItem[] = []; + private _excludedSources: Set; + private _cursorsByProvider: Map = new Map(); + private _items: { element: TreeElement }[] = []; private _loadingMessageTimer: any | undefined; private _pendingRequests = new Map(); private _uri: URI | undefined; @@ -78,13 +118,26 @@ export class TimelinePane extends ViewPane { @ITimelineService protected timelineService: ITimelineService, @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, + @ITelemetryService telemetryService: ITelemetryService, ) { - super({ ...options, titleMenuId: MenuId.TimelineTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); + super({ ...options, titleMenuId: MenuId.TimelineTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this._menus = this._register(this.instantiationService.createInstance(TimelineMenus, this.id)); const scopedContextKeyService = this._register(this.contextKeyService.createScoped()); scopedContextKeyService.createKey('view', TimelinePane.ID); + + this._excludedSources = new Set(configurationService.getValue('timeline.excludeSources')); + configurationService.onDidChangeConfiguration(this.onConfigurationChanged, this); + } + + private onConfigurationChanged(e: IConfigurationChangeEvent) { + if (!e.affectsConfiguration('timeline.excludeSources')) { + return; + } + + this._excludedSources = new Set(this.configurationService.getValue('timeline.excludeSources')); + this.loadTimeline(true); } private onActiveEditorChanged() { @@ -103,7 +156,7 @@ export class TimelinePane extends ViewPane { this._uri = uri; this._treeRenderer?.setUri(uri); - this.loadTimeline(); + this.loadTimeline(true); } private onProvidersChanged(e: TimelineProvidersChangeEvent) { @@ -114,16 +167,20 @@ export class TimelinePane extends ViewPane { } if (e.added) { - this.loadTimeline(e.added); + this.loadTimeline(true, e.added); } } private onTimelineChanged(e: TimelineChangeEvent) { - if (e.uri === undefined || e.uri.toString(true) !== this._uri?.toString(true)) { - this.loadTimeline([e.id]); + if (e?.uri === undefined || e.uri.toString(true) !== this._uri?.toString(true)) { + this.loadTimeline(e.reset ?? false, e?.id === undefined ? undefined : [e.id], { before: !e.reset }); } } + private onReset() { + this.loadTimeline(true); + } + private _message: string | undefined; get message(): string | undefined { return this._message; @@ -158,38 +215,43 @@ export class TimelinePane extends ViewPane { DOM.clearNode(this._messageElement); } - private async loadTimeline(sources?: string[]) { + private async loadTimeline(reset: boolean, sources?: string[], options: TimelineOptions = {}) { + const defaultPageSize = reset ? InitialPageSize : SubsequentPageSize; + // If we have no source, we are reseting all sources, so cancel everything in flight and reset caches if (sources === undefined) { - this._items.length = 0; + if (reset) { + this._items.length = 0; + this._cursorsByProvider.clear(); - if (this._loadingMessageTimer) { - clearTimeout(this._loadingMessageTimer); - this._loadingMessageTimer = undefined; + if (this._loadingMessageTimer) { + clearTimeout(this._loadingMessageTimer); + this._loadingMessageTimer = undefined; + } + + for (const { tokenSource } of this._pendingRequests.values()) { + tokenSource.dispose(true); + } + + this._pendingRequests.clear(); } - for (const { tokenSource } of this._pendingRequests.values()) { - tokenSource.dispose(true); - } - - this._pendingRequests.clear(); - // TODO[ECA]: Are these the right the list of schemes to exclude? Is there a better way? if (this._uri && (this._uri.scheme === 'vscode-settings' || this._uri.scheme === 'webview-panel' || this._uri.scheme === 'walkThrough')) { - this.message = 'The active editor cannot provide timeline information.'; + this.message = localize('timeline.editorCannotProvideTimeline', 'The active editor cannot provide timeline information.'); this._tree.setChildren(null, undefined); return; } - if (this._uri !== undefined) { + if (reset && this._uri !== undefined) { this._loadingMessageTimer = setTimeout((uri: URI) => { if (uri !== this._uri) { return; } this._tree.setChildren(null, undefined); - this.message = `Loading timeline for ${basename(uri.fsPath)}...`; + this.message = localize('timeline.loading', 'Loading timeline for ${0}...', basename(uri.fsPath)); }, 500, this._uri); } } @@ -198,50 +260,169 @@ export class TimelinePane extends ViewPane { return; } - for (const source of sources ?? this.timelineService.getSources()) { + const filteredSources = (sources ?? this.timelineService.getSources()).filter(s => !this._excludedSources.has(s)); + if (filteredSources.length === 0) { + if (reset) { + this.refresh(); + } + + return; + } + + let lastIndex = this._items.length - 1; + let lastItem = this._items[lastIndex]?.element; + if (isLoadMoreCommandItem(lastItem)) { + lastItem.themeIcon = { id: 'sync~spin' }; + // this._items.splice(lastIndex, 1); + lastIndex--; + + if (!reset && !options.before) { + lastItem = this._items[lastIndex]?.element; + const selection = [lastItem]; + this._tree.setSelection(selection); + this._tree.setFocus(selection); + } + } + + for (const source of filteredSources) { let request = this._pendingRequests.get(source); - request?.tokenSource.dispose(true); - request = this.timelineService.getTimeline(source, this._uri, {}, new CancellationTokenSource(), { cacheResults: true })!; + const cursors = this._cursorsByProvider.get(source); + if (!reset) { + // TODO: Handle pending request - this._pendingRequests.set(source, request); - request.tokenSource.token.onCancellationRequested(() => this._pendingRequests.delete(source)); + if (cursors?.more === false) { + continue; + } + + const reusingToken = request?.tokenSource !== undefined; + request = this.timelineService.getTimeline( + source, this._uri, + { + cursor: options.before ? cursors?.startCursors?.before : (cursors?.endCursors ?? cursors?.startCursors)?.after, + ...options, + limit: options.limit === 0 ? undefined : options.limit ?? defaultPageSize + }, + request?.tokenSource ?? new CancellationTokenSource(), { cacheResults: true } + )!; + + this._pendingRequests.set(source, request); + if (!reusingToken) { + request.tokenSource.token.onCancellationRequested(() => this._pendingRequests.delete(source)); + } + } else { + request?.tokenSource.dispose(true); + + request = this.timelineService.getTimeline( + source, this._uri, + { + ...options, + limit: options.limit === 0 ? undefined : (reset ? cursors?.endCursors?.after : undefined) ?? options.limit ?? defaultPageSize + }, + new CancellationTokenSource(), { cacheResults: true } + )!; + + this._pendingRequests.set(source, request); + request.tokenSource.token.onCancellationRequested(() => this._pendingRequests.delete(source)); + } this.handleRequest(request); } } private async handleRequest(request: TimelineRequest) { - let items; + let timeline: Timeline | undefined; try { - items = await this.progressService.withProgress({ location: VIEWLET_ID }, () => request.result.then(r => r?.items ?? [])); + timeline = await this.progressService.withProgress({ location: this.getProgressLocation() }, () => request.result); + } + finally { + this._pendingRequests.delete(request.source); } - catch { } - this._pendingRequests.delete(request.source); - if (request.tokenSource.token.isCancellationRequested || request.uri !== this._uri) { + if ( + timeline === undefined || + request.tokenSource.token.isCancellationRequested || + request.uri !== this._uri + ) { return; } - this.replaceItems(request.source, items); - } + let items: TreeElement[]; - private replaceItems(source: string, items?: TimelineItem[]) { - const hasItems = this._items.length !== 0; + const source = request.source; - if (items?.length) { - this._items.splice(0, this._items.length, ...this._items.filter(i => i.source !== source), ...items); - this._items.sort((a, b) => (b.timestamp - a.timestamp) || b.source.localeCompare(a.source, undefined, { numeric: true, sensitivity: 'base' })); + if (timeline !== undefined) { + if (timeline.paging !== undefined) { + let cursors = this._cursorsByProvider.get(timeline.source ?? source); + if (cursors === undefined) { + cursors = { startCursors: timeline.paging.cursors, more: timeline.paging.more ?? false }; + this._cursorsByProvider.set(timeline.source, cursors); + } else { + if (request.options.before) { + if (cursors.endCursors === undefined) { + cursors.endCursors = cursors.startCursors; + } + cursors.startCursors = timeline.paging.cursors; + } + else { + if (cursors.startCursors === undefined) { + cursors.startCursors = timeline.paging.cursors; + } + cursors.endCursors = timeline.paging.cursors; + } + cursors.more = timeline.paging.more ?? true; + } + } + } else { + this._cursorsByProvider.delete(source); } - else if (this._items.length && this._items.some(i => i.source === source)) { - this._items = this._items.filter(i => i.source !== source); + items = (timeline.items as TreeElement[]) ?? []; + + const alreadyHadItems = this._items.length !== 0; + + let changed; + if (request.options.cursor) { + changed = this.mergeItems(request.source, items, request.options); + } else { + changed = this.replaceItems(request.source, items); } - else { + + if (!changed) { + // If there are no items at all and no pending requests, make sure to refresh (to show the no timeline info message) + if (this._items.length === 0 && this._pendingRequests.size === 0) { + this.refresh(); + } + return; } + if (this._pendingRequests.size === 0 && this._items.length !== 0) { + const lastIndex = this._items.length - 1; + const lastItem = this._items[lastIndex]?.element; + + if (timeline.paging?.more || Iterator.some(this._cursorsByProvider.values(), cursors => cursors.more)) { + if (isLoadMoreCommandItem(lastItem)) { + lastItem.themeIcon = undefined; + } + else { + this._items.push({ + element: { + handle: 'vscode-command:loadMore', + label: localize('timeline.loadMore', 'Load more'), + timestamp: 0 + } as CommandItem + }); + } + } + else { + if (isLoadMoreCommandItem(lastItem)) { + this._items.splice(lastIndex, 1); + } + } + } + // If we have items already and there are other pending requests, debounce for a bit to wait for other requests - if (hasItems && this._pendingRequests.size !== 0) { + if (alreadyHadItems && this._pendingRequests.size !== 0) { this.refreshDebounced(); } else { @@ -249,6 +430,79 @@ export class TimelinePane extends ViewPane { } } + private mergeItems(source: string, items: TreeElement[] | undefined, options: TimelineOptions): boolean { + if (items?.length === undefined || items.length === 0) { + return false; + } + + if (options.before) { + const ids = new Set(); + const timestamps = new Set(); + + for (const item of items) { + if (item.id === undefined) { + timestamps.add(item.timestamp); + } + else { + ids.add(item.id); + } + } + + // Remove any duplicate items + // I don't think we need to check all the items, just the most recent page + let i = Math.min(SubsequentPageSize, this._items.length); + let item; + while (i--) { + item = this._items[i].element; + if ( + (item.id === undefined && ids.has(item.id)) || + (item.timestamp === undefined && timestamps.has(item.timestamp)) + ) { + this._items.splice(i, 1); + } + } + + this._items.splice(0, 0, ...items.map(item => ({ element: item }))); + } else { + this._items.push(...items.map(item => ({ element: item }))); + } + + this.sortItems(); + return true; + } + + private replaceItems(source: string, items?: TreeElement[]): boolean { + if (items?.length) { + this._items.splice( + 0, this._items.length, + ...this._items.filter(item => item.element.source !== source), + ...items.map(item => ({ element: item })) + ); + this.sortItems(); + + return true; + } + + if (this._items.length && this._items.some(item => item.element.source === source)) { + this._items = this._items.filter(item => item.element.source !== source); + + return true; + } + + return false; + } + + private sortItems() { + this._items.sort( + (a, b) => + (b.element.timestamp - a.element.timestamp) || + (a.element.source === undefined + ? b.element.source === undefined ? 0 : 1 + : b.element.source === undefined ? -1 : b.element.source.localeCompare(a.element.source, undefined, { numeric: true, sensitivity: 'base' })) + ); + + } + private refresh() { if (this._loadingMessageTimer) { clearTimeout(this._loadingMessageTimer); @@ -256,12 +510,12 @@ export class TimelinePane extends ViewPane { } if (this._items.length === 0) { - this.message = 'No timeline information was provided.'; + this.message = localize('timeline.noTimelineInfo', 'No timeline information was provided.'); } else { this.message = undefined; } - this._tree.setChildren(null, this._items.map(item => ({ element: item }))); + this._tree.setChildren(null, this._items); } @debounce(500) @@ -280,6 +534,7 @@ export class TimelinePane extends ViewPane { this.timelineService.onDidChangeProviders(this.onProvidersChanged, this, this._visibilityDisposables); this.timelineService.onDidChangeTimeline(this.onTimelineChanged, this, this._visibilityDisposables); + this.timelineService.onDidReset(this.onReset, this, this._visibilityDisposables); this.editorService.onDidActiveEditorChange(this.onActiveEditorChanged, this, this._visibilityDisposables); this.onActiveEditorChanged(); @@ -299,7 +554,7 @@ export class TimelinePane extends ViewPane { this._messageElement = DOM.append(this._container, DOM.$('.message')); DOM.addClass(this._messageElement, 'timeline-subtle'); - this.message = 'The active editor cannot provide timeline information.'; + this.message = localize('timeline.editorCannotProvideTimeline', 'The active editor cannot provide timeline information.'); this._treeElement = document.createElement('div'); DOM.addClasses(this._treeElement, 'customview-tree', 'file-icon-themable-tree', 'hide-arrows'); @@ -327,9 +582,24 @@ export class TimelinePane extends ViewPane { } const selection = this._tree.getSelection(); - const command = selection.length === 1 ? selection[0]?.command : undefined; - if (command) { - this.commandService.executeCommand(command.id, ...(command.arguments || [])); + const item = selection.length === 1 ? selection[0] : undefined; + // eslint-disable-next-line eqeqeq + if (item == null) { + return; + } + + if (isTimelineItem(item)) { + if (item.command) { + this.commandService.executeCommand(item.command.id, ...(item.command.arguments || [])); + } + } + else if (isLoadMoreCommandItem(item)) { + // TODO: Change this, but right now this is the pending signal + if (item.themeIcon !== undefined) { + return; + } + + this.loadTimeline(false); } }) ); @@ -415,6 +685,11 @@ export class TimelineIdentityProvider implements IIdentityProvider class TimelineActionRunner extends ActionRunner { runAction(action: IAction, { uri, item }: TimelineActionContext): Promise { + if (!isTimelineItem(item)) { + // TODO + return action.run(); + } + return action.run(...[ { $mid: 11, @@ -497,7 +772,7 @@ class TimelineTreeRenderer implements ITreeRenderer; - provideTimeline(uri: URI, cursor: TimelineCursor, token: CancellationToken, options?: { cacheResults?: boolean }): Promise; + provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: { cacheResults?: boolean }): Promise; } export interface TimelineProviderDescriptor { @@ -68,6 +75,7 @@ export interface TimelineProvidersChangeEvent { export interface TimelineRequest { readonly result: Promise; + readonly options: TimelineOptions; readonly source: string; readonly tokenSource: CancellationTokenSource; readonly uri: URI; @@ -78,13 +86,17 @@ export interface ITimelineService { onDidChangeProviders: Event; onDidChangeTimeline: Event; + onDidReset: Event; registerTimelineProvider(provider: TimelineProvider): IDisposable; unregisterTimelineProvider(id: string): void; getSources(): string[]; - getTimeline(id: string, uri: URI, cursor: TimelineCursor, tokenSource: CancellationTokenSource, options?: { cacheResults?: boolean }): TimelineRequest | undefined; + getTimeline(id: string, uri: URI, options: TimelineOptions, tokenSource: CancellationTokenSource, internalOptions?: { cacheResults?: boolean }): TimelineRequest | undefined; + + // refresh(fetch?: 'all' | 'more'): void; + reset(): void; } const TIMELINE_SERVICE_ID = 'timeline'; diff --git a/src/vs/workbench/contrib/timeline/common/timelineService.ts b/src/vs/workbench/contrib/timeline/common/timelineService.ts index 27038106272..d017b15245d 100644 --- a/src/vs/workbench/contrib/timeline/common/timelineService.ts +++ b/src/vs/workbench/contrib/timeline/common/timelineService.ts @@ -9,7 +9,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; // import { basename } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; -import { ITimelineService, TimelineChangeEvent, TimelineCursor, TimelineProvidersChangeEvent, TimelineProvider } from './timeline'; +import { ITimelineService, TimelineChangeEvent, TimelineOptions, TimelineProvidersChangeEvent, TimelineProvider } from './timeline'; export class TimelineService implements ITimelineService { _serviceBrand: undefined; @@ -20,6 +20,9 @@ export class TimelineService implements ITimelineService { private readonly _onDidChangeTimeline = new Emitter(); readonly onDidChangeTimeline: Event = this._onDidChangeTimeline.event; + private readonly _onDidReset = new Emitter(); + readonly onDidReset: Event = this._onDidReset.event; + private readonly _providers = new Map(); private readonly _providerSubscriptions = new Map(); @@ -81,7 +84,7 @@ export class TimelineService implements ITimelineService { return [...this._providers.keys()]; } - getTimeline(id: string, uri: URI, cursor: TimelineCursor, tokenSource: CancellationTokenSource, options?: { cacheResults?: boolean }) { + getTimeline(id: string, uri: URI, options: TimelineOptions, tokenSource: CancellationTokenSource, internalOptions?: { cacheResults?: boolean }) { this.logService.trace(`TimelineService#getTimeline(${id}): uri=${uri.toString(true)}`); const provider = this._providers.get(id); @@ -98,7 +101,7 @@ export class TimelineService implements ITimelineService { } return { - result: provider.provideTimeline(uri, cursor, tokenSource.token, options) + result: provider.provideTimeline(uri, options, tokenSource.token, internalOptions) .then(result => { if (result === undefined) { return undefined; @@ -109,6 +112,7 @@ export class TimelineService implements ITimelineService { return result; }), + options: options, source: provider.id, tokenSource: tokenSource, uri: uri @@ -156,4 +160,12 @@ export class TimelineService implements ITimelineService { this._providerSubscriptions.delete(id); this._onDidChangeProviders.fire({ removed: [id] }); } + + // refresh(fetch?: 'all' | 'more') { + // this._onDidChangeTimeline.fire({ fetch: fetch }); + // } + + reset() { + this._onDidReset.fire(); + } } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService.ts index 3caeb60f404..7e035fddbce 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService.ts @@ -10,6 +10,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { UserDataSyncTrigger } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; export class UserDataAutoSyncService extends BaseUserDataAutoSyncService { @@ -20,15 +21,15 @@ export class UserDataAutoSyncService extends BaseUserDataAutoSyncService { @IAuthenticationTokenService authTokenService: IAuthenticationTokenService, @IInstantiationService instantiationService: IInstantiationService, @IHostService hostService: IHostService, + @ITelemetryService telemetryService: ITelemetryService, ) { - super(userDataSyncEnablementService, userDataSyncService, logService, authTokenService); + super(userDataSyncEnablementService, userDataSyncService, logService, authTokenService, telemetryService); - // Sync immediately if there is a local change. - this._register(Event.debounce(Event.any( - userDataSyncService.onDidChangeLocal, + this._register(Event.debounce(Event.any( + Event.map(hostService.onDidChangeFocus, () => 'windowFocus'), instantiationService.createInstance(UserDataSyncTrigger).onDidTriggerSync, - hostService.onDidChangeFocus - ), () => undefined, 500)(() => this.triggerAutoSync())); + userDataSyncService.onDidChangeLocal, + ), (last, source) => last ? [...last, source] : [source], 1000)(sources => this.triggerAutoSync(sources))); } } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts index 5bd817b28e6..367dd0b6fb3 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts @@ -3,48 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { Registry } from 'vs/platform/registry/common/platform'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { UserDataSyncWorkbenchContribution } from 'vs/workbench/contrib/userDataSync/browser/userDataSync'; -import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; -import { IUserDataSyncEnablementService, getUserDataSyncStore } from 'vs/platform/userDataSync/common/userDataSync'; - -class UserDataSyncSettingsMigrationContribution implements IWorkbenchContribution { - - constructor( - @IConfigurationService private readonly configurationService: IConfigurationService, - @IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService, - ) { - if (getUserDataSyncStore(configurationService)) { - if (!configurationService.getValue('sync.enableSettings')) { - userDataSyncEnablementService.setResourceEnablement('settings', false); - } - if (!configurationService.getValue('sync.enableKeybindings')) { - userDataSyncEnablementService.setResourceEnablement('keybindings', false); - } - if (!configurationService.getValue('sync.enableUIState')) { - userDataSyncEnablementService.setResourceEnablement('globalState', false); - } - if (!configurationService.getValue('sync.enableExtensions')) { - userDataSyncEnablementService.setResourceEnablement('extensions', false); - } - if (configurationService.getValue('sync.enable')) { - userDataSyncEnablementService.setEnablement(true); - } - this.removeFromConfiguration(); - } - } - - private async removeFromConfiguration(): Promise { - await this.configurationService.updateValue('sync.enable', undefined, ConfigurationTarget.USER); - await this.configurationService.updateValue('sync.enableSettings', undefined, ConfigurationTarget.USER); - await this.configurationService.updateValue('sync.enableKeybindings', undefined, ConfigurationTarget.USER); - await this.configurationService.updateValue('sync.enableUIState', undefined, ConfigurationTarget.USER); - await this.configurationService.updateValue('sync.enableExtensions', undefined, ConfigurationTarget.USER); - } -} const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(UserDataSyncWorkbenchContribution, LifecyclePhase.Ready); -workbenchRegistry.registerWorkbenchContribution(UserDataSyncSettingsMigrationContribution, LifecyclePhase.Ready); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index df54d4357c9..8bf9147ddcb 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -47,6 +47,8 @@ import { IPreferencesService } from 'vs/workbench/services/preferences/common/pr import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication'; import { fromNow } from 'vs/base/common/date'; import { IProductService } from 'vs/platform/product/common/productService'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; const enum AuthStatus { Initializing = 'Initializing', @@ -86,15 +88,18 @@ const getActivityTitle = (label: string, userDataSyncService: IUserDataSyncServi } return label; }; +const getIdentityTitle = (label: string, authenticationProviderId: string, account: AuthenticationSession | undefined, authenticationService: IAuthenticationService): string => { + return account ? `${label} (${authenticationService.getDisplayName(authenticationProviderId)}:${account.accountName})` : label; +}; const turnOnSyncCommand = { id: 'workbench.userData.actions.syncStart', title: localize('turn on sync with category', "Sync: Turn on Sync") }; const signInCommand = { id: 'workbench.userData.actions.signin', title: localize('sign in', "Sync: Sign in to sync") }; -const stopSyncCommand = { id: 'workbench.userData.actions.stopSync', title: localize('stop sync', "Sync: Turn off Sync") }; +const stopSyncCommand = { id: 'workbench.userData.actions.stopSync', title(authenticationProviderId: string, account: AuthenticationSession | undefined, authenticationService: IAuthenticationService) { return getIdentityTitle(localize('stop sync', "Sync: Turn off Sync"), authenticationProviderId, account, authenticationService); } }; const resolveSettingsConflictsCommand = { id: 'workbench.userData.actions.resolveSettingsConflicts', title: localize('showConflicts', "Sync: Show Settings Conflicts") }; const resolveKeybindingsConflictsCommand = { id: 'workbench.userData.actions.resolveKeybindingsConflicts', title: localize('showKeybindingsConflicts', "Sync: Show Keybindings Conflicts") }; const configureSyncCommand = { id: 'workbench.userData.actions.configureSync', title: localize('configure sync', "Sync: Configure") }; const showSyncActivityCommand = { id: 'workbench.userData.actions.showSyncActivity', title(userDataSyncService: IUserDataSyncService): string { - return getActivityTitle(localize('show sync log', "Sync: Show Activity"), userDataSyncService); + return getActivityTitle(localize('show sync log', "Sync: Show Log"), userDataSyncService); } }; const showSyncSettingsCommand = { id: 'workbench.userData.actions.syncSettings', title: localize('sync settings', "Sync: Settings"), }; @@ -106,13 +111,11 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private readonly syncStatusContext: IContextKey; private readonly authenticationState: IContextKey; private readonly conflictsSources: IContextKey; - private readonly conflictsDisposables = new Map(); + private readonly badgeDisposable = this._register(new MutableDisposable()); private readonly signInNotificationDisposable = this._register(new MutableDisposable()); private _activeAccount: AuthenticationSession | undefined; - private readonly syncStatusAction = this._register(new MutableDisposable()); - constructor( @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService, @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, @@ -134,9 +137,11 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo @ITelemetryService private readonly telemetryService: ITelemetryService, @IFileService private readonly fileService: IFileService, @IProductService private readonly productService: IProductService, + @IStorageService private readonly storageService: IStorageService, + @IOpenerService private readonly openerService: IOpenerService, ) { super(); - this.userDataSyncStore = getUserDataSyncStore(configurationService); + this.userDataSyncStore = getUserDataSyncStore(productService, configurationService); this.syncEnablementContext = CONTEXT_SYNC_ENABLEMENT.bindTo(contextKeyService); this.syncStatusContext = CONTEXT_SYNC_STATE.bindTo(contextKeyService); this.authenticationState = CONTEXT_AUTH_TOKEN_STATE.bindTo(contextKeyService); @@ -148,6 +153,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo this.onDidChangeEnablement(this.userDataSyncEnablementService.isEnabled()); this._register(Event.debounce(userDataSyncService.onDidChangeStatus, () => undefined, 500)(() => this.onDidChangeSyncStatus(this.userDataSyncService.status))); this._register(userDataSyncService.onDidChangeConflicts(() => this.onDidChangeConflicts(this.userDataSyncService.conflictsSources))); + this._register(userDataSyncService.onSyncErrors(errors => this.onSyncErrors(errors))); this._register(this.authTokenService.onTokenFailed(_ => this.authenticationService.getSessions(this.userDataSyncStore!.authenticationProviderId))); this._register(this.userDataSyncEnablementService.onDidChangeEnablement(enabled => this.onDidChangeEnablement(enabled))); this._register(this.authenticationService.onDidRegisterAuthenticationProvider(e => this.onDidRegisterAuthenticationProvider(e))); @@ -157,7 +163,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo this.registerActions(); this.initializeActiveAccount().then(_ => { if (!isWeb) { - this._register(instantiationService.createInstance(UserDataSyncTrigger).onDidTriggerSync(() => userDataAutoSyncService.triggerAutoSync())); + this._register(instantiationService.createInstance(UserDataSyncTrigger).onDidTriggerSync(source => userDataAutoSyncService.triggerAutoSync([source]))); } }); @@ -266,6 +272,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo this.updateBadge(); } + private readonly conflictsDisposables = new Map(); private onDidChangeConflicts(conflicts: SyncSource[]) { this.updateBadge(); if (conflicts.length) { @@ -283,7 +290,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo const conflictsEditorInput = this.getConflictsEditorInput(conflictsSource); if (!conflictsEditorInput && !this.conflictsDisposables.has(conflictsSource)) { const conflictsArea = getSyncAreaLabel(conflictsSource); - const handle = this.notificationService.prompt(Severity.Warning, localize('conflicts detected', "Unable to sync due to conflicts in {0}. Please resolve them to continue.", conflictsArea), + const handle = this.notificationService.prompt(Severity.Warning, localize('conflicts detected', "Unable to sync due to conflicts in {0}. Please resolve them to continue.", conflictsArea.toLowerCase()), [ { label: localize('accept remote', "Accept Remote"), @@ -390,20 +397,21 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo case UserDataSyncErrorCode.SessionExpired: this.notificationService.notify({ severity: Severity.Info, - message: localize('turned off', "Turned off sync because it was turned off from other device."), + message: localize('turned off', "Sync was turned off from another device."), actions: { - primary: [new Action('turn on sync', localize('Turn on sync', "Turn on Sync"), undefined, true, () => this.turnOn())] + primary: [new Action('turn on sync', localize('turn on sync', "Turn on Sync"), undefined, true, () => this.turnOn())] } }); return; case UserDataSyncErrorCode.TooLarge: if (error.source === SyncSource.Keybindings || error.source === SyncSource.Settings) { + this.disableSync(error.source); const sourceArea = getSyncAreaLabel(error.source); this.notificationService.notify({ severity: Severity.Error, - message: localize('too large', "Disabled sync {0} because size of the {1} file to sync is larger than {2}. Please open the file and reduce the size and enable sync", sourceArea, sourceArea, '100kb'), + message: localize('too large', "Disabled syncing {0} because size of the {1} file to sync is larger than {2}. Please open the file and reduce the size and enable sync", sourceArea.toLowerCase(), sourceArea.toLowerCase(), '100kb'), actions: { - primary: [new Action('open sync file', localize('open file', "Show {0} file", sourceArea), undefined, true, + primary: [new Action('open sync file', localize('open file', "Open {0} File", sourceArea), undefined, true, () => error.source === SyncSource.Settings ? this.preferencesService.openGlobalSettings(true) : this.preferencesService.openGlobalKeybindingSettings(true))] } }); @@ -419,6 +427,56 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } } + private readonly invalidContentErrorDisposables = new Map(); + private onSyncErrors(errors: [SyncSource, UserDataSyncError][]): void { + if (errors.length) { + for (const [source, error] of errors) { + switch (error.code) { + case UserDataSyncErrorCode.LocalInvalidContent: + this.handleInvalidContentError(source); + break; + default: + const disposable = this.invalidContentErrorDisposables.get(source); + if (disposable) { + disposable.dispose(); + this.invalidContentErrorDisposables.delete(source); + } + } + } + } else { + this.invalidContentErrorDisposables.forEach(disposable => disposable.dispose()); + this.invalidContentErrorDisposables.clear(); + } + } + + private handleInvalidContentError(source: SyncSource): void { + if (this.invalidContentErrorDisposables.has(source)) { + return; + } + if (source !== SyncSource.Settings && source !== SyncSource.Keybindings) { + return; + } + const resource = source === SyncSource.Settings ? this.workbenchEnvironmentService.settingsResource : this.workbenchEnvironmentService.keybindingsResource; + if (isEqual(resource, this.editorService.activeEditor?.resource)) { + // Do not show notification if the file in error is active + return; + } + const errorArea = getSyncAreaLabel(source); + const handle = this.notificationService.notify({ + severity: Severity.Error, + message: localize('errorInvalidConfiguration', "Unable to sync {0} because there are some errors/warnings in the file. Please open the file to correct errors/warnings in it.", errorArea.toLowerCase()), + actions: { + primary: [new Action('open sync file', localize('open file', "Open {0} File", errorArea), undefined, true, + () => source === SyncSource.Settings ? this.preferencesService.openGlobalSettings(true) : this.preferencesService.openGlobalKeybindingSettings(true))] + } + }); + this.invalidContentErrorDisposables.set(source, toDisposable(() => { + // close the error warning notification + handle.close(); + this.invalidContentErrorDisposables.delete(source); + })); + } + private async updateBadge(): Promise { this.badgeDisposable.clear(); @@ -438,18 +496,36 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } private async turnOn(): Promise { + if (!this.storageService.getBoolean('sync.donotAskPreviewConfirmation', StorageScope.GLOBAL, false)) { + const result = await this.dialogService.show( + Severity.Info, + localize('sync preview message', "Synchronizing your preferences is a preview feature, please read the documentation before turning it on."), + [ + localize('open doc', "Open Documentation"), + localize('turn on sync', "Turn on Sync"), + localize('cancel', "Cancel"), + ], + { + cancelId: 2 + } + ); + switch (result.choice) { + case 0: this.openerService.open(URI.parse('https://go.microsoft.com/fwlink/?LinkId=827846')); return; + case 2: return; + } + } return new Promise((c, e) => { const disposables: DisposableStore = new DisposableStore(); const quickPick = this.quickInputService.createQuickPick(); disposables.add(quickPick); - quickPick.title = localize('turn on sync', "Turn on Sync"); + quickPick.title = localize('turn on title', "Sync: Turn On"); quickPick.ok = false; quickPick.customButton = true; if (this.authenticationState.get() === AuthStatus.SignedIn) { quickPick.customLabel = localize('turn on', "Turn on"); } else { const displayName = this.authenticationService.getDisplayName(this.userDataSyncStore!.authenticationProviderId); - quickPick.description = localize('sign in and turn on sync detail', "Please sign in with your {0} account to synchronize your following data across all your devices.", displayName); + quickPick.description = localize('sign in and turn on sync detail', "Sign in with your {0} account to synchronize your data across devices.", displayName); quickPick.customLabel = localize('sign in and turn on sync', "Sign in & Turn on"); } quickPick.placeholder = localize('configure sync placeholder', "Choose what to sync"); @@ -471,12 +547,46 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } private async doTurnOn(): Promise { + if (this.authenticationState.get() === AuthStatus.SignedIn) { + await new Promise((c, e) => { + const disposables: DisposableStore = new DisposableStore(); + const displayName = this.authenticationService.getDisplayName(this.userDataSyncStore!.authenticationProviderId); + const quickPick = this.quickInputService.createQuickPick<{ id: string, label: string, description?: string, detail?: string }>(); + const chooseAnotherItemId = 'chooseAnother'; + disposables.add(quickPick); + quickPick.title = localize('pick account', "{0}: Pick an account", displayName); + quickPick.ok = false; + quickPick.placeholder = localize('choose account placeholder', "Pick an account for syncing"); + quickPick.ignoreFocusOut = true; + quickPick.items = [{ + id: 'existing', + label: localize('existing', "{0}", this.activeAccount!.accountName), + detail: localize('signed in', "Signed in"), + }, { + id: chooseAnotherItemId, + label: localize('choose another', "Use another account") + }]; + disposables.add(quickPick.onDidAccept(async () => { + if (quickPick.selectedItems.length) { + if (quickPick.selectedItems[0].id === chooseAnotherItemId) { + await this.authenticationService.logout(this.userDataSyncStore!.authenticationProviderId, this.activeAccount!.id); + await this.setActiveAccount(undefined); + } + quickPick.hide(); + c(); + } + })); + disposables.add(quickPick.onDidHide(() => disposables.dispose())); + quickPick.show(); + }); + } if (this.authenticationState.get() === AuthStatus.SignedOut) { await this.signIn(); } await this.handleFirstTimeSync(); this.userDataSyncEnablementService.setEnablement(true); - this.notificationService.info(localize('sync turned on', "Sync is turned on and from now on sycing will be done automatically.")); + this.notificationService.info(localize('sync turned on', "Sync will happen automatically from now on.")); + this.storageService.store('sync.donotAskPreviewConfirmation', true, StorageScope.GLOBAL); } private getConfigureSyncQuickPickItems(): ConfigureSyncQuickPickItem[] { @@ -540,7 +650,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } const result = await this.dialogService.show( Severity.Info, - localize('firs time sync', "First time Sync"), + localize('firs time sync', "Sync"), [ localize('merge', "Merge"), localize('cancel', "Cancel"), @@ -548,7 +658,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo ], { cancelId: 1, - detail: localize('first time sync detail', "Synchronizing from this device for the first time.\nWould you like to merge or replace with the data from the cloud?"), + detail: localize('first time sync detail', "It looks like this is the first time sync is set up.\nWould you like to merge or replace with the data from the cloud?"), } ); switch (result.choice) { @@ -700,7 +810,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private registerSignInAction(): void { const that = this; - registerAction2(class StopSyncAction extends Action2 { + this._register(registerAction2(class StopSyncAction extends Action2 { constructor() { super({ id: signInCommand.id, @@ -720,7 +830,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo that.notificationService.error(e); } } - }); + })); } private registerShowSettingsConflictsAction(): void { @@ -780,22 +890,24 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private registerSyncStatusAction(): void { const that = this; - const id = 'workbench.userData.actions.syncStatus'; - const title = localize('sync is on', "Sync is on"); const when = ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, CONTEXT_AUTH_TOKEN_STATE.isEqualTo(AuthStatus.SignedIn), CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized)); - this.syncStatusAction.value = registerAction2(class SyncStatusAction extends Action2 { + this._register(registerAction2(class SyncStatusAction extends Action2 { constructor() { super({ - id, - get title() { - return getActivityTitle(localize('sync is on', "Sync is on"), that.userDataSyncService); - }, + id: 'workbench.userData.actions.syncStatus', + title: localize('sync is on', "Sync is on"), menu: [ { id: MenuId.GlobalActivity, group: '5_sync', when, order: 3 + }, + { + id: MenuId.MenubarPreferencesMenu, + group: '5_sync', + when, + order: 3, } ], }); @@ -823,7 +935,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo items.push({ id: showSyncSettingsCommand.id, label: showSyncSettingsCommand.title }); items.push({ id: showSyncActivityCommand.id, label: showSyncActivityCommand.title(that.userDataSyncService) }); items.push({ type: 'separator' }); - items.push({ id: stopSyncCommand.id, label: stopSyncCommand.title }); + items.push({ id: stopSyncCommand.id, label: stopSyncCommand.title(that.userDataSyncStore!.authenticationProviderId, that.activeAccount, that.authenticationService) }); quickPick.items = items; const disposables = new DisposableStore(); disposables.add(quickPick.onDidAccept(() => { @@ -839,22 +951,16 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo quickPick.show(); }); } - }); - MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { - group: '5_sync', - command: { id, title }, - when, - order: 3 - }); + })); } private registerTurnOffSyncAction(): void { const that = this; - registerAction2(class StopSyncAction extends Action2 { + this._register(registerAction2(class StopSyncAction extends Action2 { constructor() { super({ id: stopSyncCommand.id, - title: stopSyncCommand.title, + title: stopSyncCommand.title(that.userDataSyncStore!.authenticationProviderId, that.activeAccount, that.authenticationService), menu: { id: MenuId.CommandPalette, when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT), @@ -870,12 +976,12 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } } } - }); + })); } private registerConfigureSyncAction(): void { const that = this; - registerAction2(class ShowSyncActivityAction extends Action2 { + this._register(registerAction2(class ShowSyncActivityAction extends Action2 { constructor() { super({ id: configureSyncCommand.id, @@ -887,16 +993,16 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo }); } run(): any { return that.configureSyncOptions(); } - }); + })); } private registerShowActivityAction(): void { const that = this; - registerAction2(class ShowSyncActivityAction extends Action2 { + this._register(registerAction2(class ShowSyncActivityAction extends Action2 { constructor() { super({ id: showSyncActivityCommand.id, - title: showSyncActivityCommand.title(that.userDataSyncService), + get title() { return showSyncActivityCommand.title(that.userDataSyncService); }, menu: { id: MenuId.CommandPalette, when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized)), @@ -904,11 +1010,11 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo }); } run(): any { return that.showSyncActivity(); } - }); + })); } private registerShowSettingsAction(): void { - registerAction2(class ShowSyncSettingsAction extends Action2 { + this._register(registerAction2(class ShowSyncSettingsAction extends Action2 { constructor() { super({ id: showSyncSettingsCommand.id, @@ -920,9 +1026,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo }); } run(accessor: ServicesAccessor): any { - accessor.get(IPreferencesService).openGlobalSettings(false, { query: 'sync:' }); + accessor.get(IPreferencesService).openGlobalSettings(false, { query: '@tag:sync' }); } - }); + })); } } @@ -1027,8 +1133,8 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio ? localize('Sync accept remote', "Sync: {0}", acceptRemoteLabel) : localize('Sync accept local', "Sync: {0}", acceptLocalLabel), message: isRemote - ? localize('confirm replace and overwrite local', "Would you like to accept Remote {0} and replace Local {1}?", syncAreaLabel, syncAreaLabel) - : localize('confirm replace and overwrite remote', "Would you like to accept Local {0} and replace Remote {1}?", syncAreaLabel, syncAreaLabel), + ? localize('confirm replace and overwrite local', "Would you like to accept Remote {0} and replace Local {1}?", syncAreaLabel.toLowerCase(), syncAreaLabel.toLowerCase()) + : localize('confirm replace and overwrite remote', "Would you like to accept Local {0} and replace Remote {1}?", syncAreaLabel.toLowerCase(), syncAreaLabel.toLowerCase()), primaryButton: isRemote ? acceptRemoteLabel : acceptLocalLabel }); if (result.confirmed) { diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts index 43b93c20ab2..1642cd210a4 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts @@ -16,8 +16,8 @@ import { IViewlet } from 'vs/workbench/common/viewlet'; export class UserDataSyncTrigger extends Disposable { - private readonly _onDidTriggerSync: Emitter = this._register(new Emitter()); - readonly onDidTriggerSync: Event = this._onDidTriggerSync.event; + private readonly _onDidTriggerSync: Emitter = this._register(new Emitter()); + readonly onDidTriggerSync: Event = this._onDidTriggerSync.event; constructor( @IEditorService editorService: IEditorService, @@ -25,37 +25,44 @@ export class UserDataSyncTrigger extends Disposable { @IViewletService viewletService: IViewletService, ) { super(); - this._register(Event.debounce(Event.any( - Event.filter(editorService.onDidActiveEditorChange, () => this.isUserDataEditorInput(editorService.activeEditor)), - Event.filter(viewletService.onDidViewletOpen, viewlet => this.isUserDataViewlet(viewlet)) - ), () => undefined, 500)(() => this._onDidTriggerSync.fire())); + this._register(Event.any( + Event.map(editorService.onDidActiveEditorChange, () => this.getUserDataEditorInputSource(editorService.activeEditor)), + Event.map(viewletService.onDidViewletOpen, viewlet => this.getUserDataViewletSource(viewlet)) + )(source => { + if (source) { + this._onDidTriggerSync.fire(source); + } + })); } - private isUserDataViewlet(viewlet: IViewlet): boolean { - return viewlet.getId() === VIEWLET_ID; + private getUserDataViewletSource(viewlet: IViewlet): string | undefined { + if (viewlet.getId() === VIEWLET_ID) { + return 'extensionsViewlet'; + } + return undefined; } - private isUserDataEditorInput(editorInput: IEditorInput | undefined): boolean { + private getUserDataEditorInputSource(editorInput: IEditorInput | undefined): string | undefined { if (!editorInput) { - return false; + return undefined; } if (editorInput instanceof SettingsEditor2Input) { - return true; + return 'settingsEditor'; } if (editorInput instanceof PreferencesEditorInput) { - return true; + return 'settingsEditor'; } if (editorInput instanceof KeybindingsEditorInput) { - return true; + return 'keybindingsEditor'; } const resource = editorInput.resource; if (isEqual(resource, this.workbenchEnvironmentService.settingsResource)) { - return true; + return 'settingsEditor'; } if (isEqual(resource, this.workbenchEnvironmentService.keybindingsResource)) { - return true; + return 'keybindingsEditor'; } - return false; + return undefined; } } diff --git a/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts b/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts index 77e239607a4..1edea0078b3 100644 --- a/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts +++ b/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts @@ -12,7 +12,8 @@ import * as path from 'vs/base/common/path'; import * as pfs from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; import { BackupFilesModel } from 'vs/workbench/services/backup/common/backupFileService'; -import { TextModel, createTextBufferFactory } from 'vs/editor/common/model/textModel'; +import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { DefaultEndOfLine, ITextSnapshot } from 'vs/editor/common/model'; import { Schemas } from 'vs/base/common/network'; @@ -205,7 +206,7 @@ suite('BackupFileService', () => { }); test('text file (ITextSnapshot)', async () => { - const model = TextModel.createFromString('test'); + const model = createTextModel('test'); await service.backup(fooFile, model.createSnapshot()); assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1); @@ -217,7 +218,7 @@ suite('BackupFileService', () => { }); test('untitled file (ITextSnapshot)', async () => { - const model = TextModel.createFromString('test'); + const model = createTextModel('test'); await service.backup(untitledFile, model.createSnapshot()); assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1); @@ -229,7 +230,7 @@ suite('BackupFileService', () => { test('text file (large file, ITextSnapshot)', async () => { const largeString = (new Array(10 * 1024)).join('Large String\n'); - const model = TextModel.createFromString(largeString); + const model = createTextModel(largeString); await service.backup(fooFile, model.createSnapshot()); assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1); @@ -242,7 +243,7 @@ suite('BackupFileService', () => { test('untitled file (large file, ITextSnapshot)', async () => { const largeString = (new Array(10 * 1024)).join('Large String\n'); - const model = TextModel.createFromString(largeString); + const model = createTextModel(largeString); await service.backup(untitledFile, model.createSnapshot()); assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1); diff --git a/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts b/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts index 414a94ac93b..e5b490f3187 100644 --- a/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts +++ b/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts @@ -10,6 +10,7 @@ import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IBulkEditOptions, IBulkEditResult, IBulkEditService, IBulkEditPreviewHandler } from 'vs/editor/browser/services/bulkEditService'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; import { EndOfLineSequence, IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model'; import { WorkspaceFileEdit, WorkspaceTextEdit, WorkspaceEdit } from 'vs/editor/common/modes'; import { IModelService } from 'vs/editor/common/services/modelService'; @@ -18,7 +19,7 @@ import { localize } from 'vs/nls'; import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; -import { IProgress, IProgressStep, emptyProgress } from 'vs/platform/progress/common/progress'; +import { IProgress, IProgressStep, Progress } from 'vs/platform/progress/common/progress'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -26,20 +27,21 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; - +import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; +import { EditStackElement, MultiModelEditStackElement } from 'vs/editor/common/model/editStack'; type ValidationResult = { canApply: true } | { canApply: false, reason: URI }; class ModelEditTask implements IDisposable { - private readonly _model: ITextModel; + public readonly model: ITextModel; protected _edits: IIdentifiedSingleEditOperation[]; private _expectedModelVersionId: number | undefined; protected _newEol: EndOfLineSequence | undefined; constructor(private readonly _modelReference: IReference) { - this._model = this._modelReference.object.textEditorModel; + this.model = this._modelReference.object.textEditorModel; this._edits = []; } @@ -67,7 +69,7 @@ class ModelEditTask implements IDisposable { // create edit operation let range: Range; if (!edit.range) { - range = this._model.getFullModelRange(); + range = this.model.getFullModelRange(); } else { range = Range.lift(edit.range); } @@ -75,23 +77,23 @@ class ModelEditTask implements IDisposable { } validate(): ValidationResult { - if (typeof this._expectedModelVersionId === 'undefined' || this._model.getVersionId() === this._expectedModelVersionId) { + if (typeof this._expectedModelVersionId === 'undefined' || this.model.getVersionId() === this._expectedModelVersionId) { return { canApply: true }; } - return { canApply: false, reason: this._model.uri }; + return { canApply: false, reason: this.model.uri }; + } + + getBeforeCursorState(): Selection[] | null { + return null; } apply(): void { if (this._edits.length > 0) { this._edits = mergeSort(this._edits, (a, b) => Range.compareRangesUsingStarts(a.range, b.range)); - this._model.pushStackElement(); - this._model.pushEditOperations([], this._edits, () => []); - this._model.pushStackElement(); + this.model.pushEditOperations(null, this._edits, () => null); } if (this._newEol !== undefined) { - this._model.pushStackElement(); - this._model.pushEOL(this._newEol); - this._model.pushStackElement(); + this.model.pushEOL(this._newEol); } } } @@ -105,18 +107,18 @@ class EditorEditTask extends ModelEditTask { this._editor = editor; } + getBeforeCursorState(): Selection[] | null { + return this._editor.getSelections(); + } + apply(): void { if (this._edits.length > 0) { this._edits = mergeSort(this._edits, (a, b) => Range.compareRangesUsingStarts(a.range, b.range)); - this._editor.pushUndoStop(); this._editor.executeEdits('', this._edits); - this._editor.pushUndoStop(); } if (this._newEol !== undefined) { if (this._editor.hasModel()) { - this._editor.pushUndoStop(); this._editor.getModel().pushEOL(this._newEol); - this._editor.pushUndoStop(); } } } @@ -128,11 +130,13 @@ class BulkEditModel implements IDisposable { private _tasks: ModelEditTask[] | undefined; constructor( + private readonly _label: string | undefined, private readonly _editor: ICodeEditor | undefined, private readonly _progress: IProgress, edits: WorkspaceTextEdit[], @IEditorWorkerService private readonly _editorWorker: IEditorWorkerService, @ITextModelService private readonly _textModelResolverService: ITextModelService, + @IUndoRedoService private readonly _undoRedoService: IUndoRedoService ) { edits.forEach(this._addEdit, this); } @@ -215,10 +219,31 @@ class BulkEditModel implements IDisposable { } apply(): void { - for (const task of this._tasks!) { + const tasks = this._tasks!; + + if (tasks.length === 1) { + // This edit touches a single model => keep things simple + for (const task of tasks) { + task.model.pushStackElement(); + task.apply(); + task.model.pushStackElement(); + this._progress.report(undefined); + } + return; + } + + const multiModelEditStackElement = new MultiModelEditStackElement( + this._label || localize('workspaceEdit', "Workspace Edit"), + tasks.map(t => new EditStackElement(t.model, t.getBeforeCursorState())) + ); + this._undoRedoService.pushElement(multiModelEditStackElement); + + for (const task of tasks) { task.apply(); this._progress.report(undefined); } + + multiModelEditStackElement.close(); } } @@ -226,11 +251,13 @@ type Edit = WorkspaceFileEdit | WorkspaceTextEdit; class BulkEdit { + private readonly _label: string | undefined; private readonly _edits: Edit[] = []; private readonly _editor: ICodeEditor | undefined; private readonly _progress: IProgress; constructor( + label: string | undefined, editor: ICodeEditor | undefined, progress: IProgress | undefined, edits: Edit[], @@ -241,8 +268,9 @@ class BulkEdit { @IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService, @IConfigurationService private readonly _configurationService: IConfigurationService ) { + this._label = label; this._editor = editor; - this._progress = progress || emptyProgress; + this._progress = progress || Progress.None; this._edits = edits; } @@ -337,7 +365,7 @@ class BulkEdit { private async _performTextEdits(edits: WorkspaceTextEdit[], progress: IProgress): Promise { this._logService.debug('_performTextEdits', JSON.stringify(edits)); - const model = this._instaService.createInstance(BulkEditModel, this._editor, progress, edits); + const model = this._instaService.createInstance(BulkEditModel, this._label, this._editor, progress, edits); await model.prepare(); @@ -415,7 +443,7 @@ export class BulkEditService implements IBulkEditService { // If the code editor is readonly still allow bulk edits to be applied #68549 codeEditor = undefined; } - const bulkEdit = this._instaService.createInstance(BulkEdit, codeEditor, options?.progress, edits); + const bulkEdit = this._instaService.createInstance(BulkEdit, options?.label, codeEditor, options?.progress, edits); return bulkEdit.perform().then(() => { return { ariaSummary: bulkEdit.ariaMessage() }; }).catch(err => { diff --git a/src/vs/workbench/services/configurationResolver/common/configurationResolverSchema.ts b/src/vs/workbench/services/configurationResolver/common/configurationResolverSchema.ts index 6350d247915..2fceba9e4ac 100644 --- a/src/vs/workbench/services/configurationResolver/common/configurationResolverSchema.ts +++ b/src/vs/workbench/services/configurationResolver/common/configurationResolverSchema.ts @@ -46,7 +46,7 @@ export const inputsSchema: IJSONSchema = { }, password: { type: 'boolean', - description: nls.localize('JsonSchema.input.password', "Set to true to show a password prompt that will not show the typed value."), + description: nls.localize('JsonSchema.input.password', "Controls if a password input is shown. Password input hides the typed text."), }, } }, diff --git a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts index d481f2ee6ff..bdcff69df4f 100644 --- a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts @@ -92,6 +92,10 @@ export abstract class AbstractFileDialogService implements IFileDialogService { return ConfirmResult.DONT_SAVE; // no veto when we are in extension dev mode because we cannot assume we run interactive (e.g. tests) } + return this.doShowSaveConfirm(fileNamesOrResources); + } + + protected async doShowSaveConfirm(fileNamesOrResources: (string | URI)[]): Promise { if (fileNamesOrResources.length === 0) { return ConfirmResult.DONT_SAVE; } diff --git a/src/vs/workbench/services/dialogs/electron-browser/fileDialogService.ts b/src/vs/workbench/services/dialogs/electron-browser/fileDialogService.ts index 14b4f05f932..36e903296cb 100644 --- a/src/vs/workbench/services/dialogs/electron-browser/fileDialogService.ts +++ b/src/vs/workbench/services/dialogs/electron-browser/fileDialogService.ts @@ -198,7 +198,7 @@ export class FileDialogService extends AbstractFileDialogService implements IFil } } - return super.showSaveConfirm(fileNamesOrResources); + return super.doShowSaveConfirm(fileNamesOrResources); } } diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index 9c352fdb917..f09022e3a4f 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -286,7 +286,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { replacement: { ...moveResult.editor, options: { - ...(moveResult.editor as IResourceEditor /* TS fail */).options, + ...moveResult.editor.options, ...optionOverrides } } diff --git a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts index 6f05876c24c..4066348ec73 100644 --- a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts @@ -5,89 +5,23 @@ import * as assert from 'assert'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; -import { workbenchInstantiationService, TestStorageService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { workbenchInstantiationService, registerTestEditor, TestFileEditorInput } from 'vs/workbench/test/browser/workbenchTestServices'; import { GroupDirection, GroupsOrder, MergeGroupMode, GroupOrientation, GroupChangeKind, GroupLocation } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { EditorInput, IFileEditorInput, IEditorInputFactory, IEditorInputFactoryRegistry, Extensions as EditorExtensions, EditorOptions, CloseDirection, IEditorPartOptions, EditorsOrder } from 'vs/workbench/common/editor'; -import { IEditorModel } from 'vs/platform/editor/common/editor'; +import { EditorOptions, CloseDirection, IEditorPartOptions, EditorsOrder } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { IEditorRegistry, Extensions, EditorDescriptor } from 'vs/workbench/browser/editor'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { CancellationToken } from 'vs/base/common/cancellation'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; const TEST_EDITOR_ID = 'MyFileEditorForEditorGroupService'; const TEST_EDITOR_INPUT_ID = 'testEditorInputForEditorGroupService'; -class TestEditorControl extends BaseEditor { - - constructor(@ITelemetryService telemetryService: ITelemetryService) { super(TEST_EDITOR_ID, NullTelemetryService, new TestThemeService(), new TestStorageService()); } - - async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { - super.setInput(input, options, token); - - await input.resolve(); - } - - getId(): string { return TEST_EDITOR_ID; } - layout(): void { } - createEditor(): any { } -} - -class TestEditorInput extends EditorInput implements IFileEditorInput { - - constructor(public resource: URI) { super(); } - - getTypeId() { return TEST_EDITOR_INPUT_ID; } - resolve(): Promise { return Promise.resolve(null); } - matches(other: TestEditorInput): boolean { return other && this.resource.toString() === other.resource.toString() && other instanceof TestEditorInput; } - setEncoding(encoding: string) { } - getEncoding() { return undefined; } - setPreferredEncoding(encoding: string) { } - setMode(mode: string) { } - setPreferredMode(mode: string) { } - setForceOpenAsBinary(): void { } - isResolved(): boolean { return false; } -} - suite('EditorGroupsService', () => { let disposables: IDisposable[] = []; setup(() => { - interface ISerializedTestEditorInput { - resource: string; - } - - class TestEditorInputFactory implements IEditorInputFactory { - - canSerialize(editorInput: EditorInput): boolean { - return true; - } - - serialize(editorInput: EditorInput): string { - const testEditorInput = editorInput; - const testInput: ISerializedTestEditorInput = { - resource: testEditorInput.resource.toString() - }; - - return JSON.stringify(testInput); - } - - deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput { - const testInput: ISerializedTestEditorInput = JSON.parse(serializedEditorInput); - - return new TestEditorInput(URI.parse(testInput.resource)); - } - } - - disposables.push((Registry.as(EditorExtensions.EditorInputFactories)).registerEditorInputFactory(TEST_EDITOR_INPUT_ID, TestEditorInputFactory)); - disposables.push((Registry.as(Extensions.Editors)).registerEditor(EditorDescriptor.create(TestEditorControl, TEST_EDITOR_ID, 'My Test File Editor'), [new SyncDescriptor(TestEditorInput)])); + disposables.push(registerTestEditor(TEST_EDITOR_ID, [new SyncDescriptor(TestFileEditorInput)], TEST_EDITOR_INPUT_ID)); }); teardown(() => { @@ -344,17 +278,17 @@ suite('EditorGroupsService', () => { rootGroupDisposed = true; }); - const input = new TestEditorInput(URI.file('foo/bar')); + const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); await rootGroup.openEditor(input, EditorOptions.create({ pinned: true })); const rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT, { activate: true }); const downGroup = part.copyGroup(rootGroup, rightGroup, GroupDirection.DOWN); assert.equal(groupAddedCounter, 2); assert.equal(downGroup.count, 1); - assert.ok(downGroup.activeEditor instanceof TestEditorInput); + assert.ok(downGroup.activeEditor instanceof TestFileEditorInput); part.mergeGroup(rootGroup, rightGroup, { mode: MergeGroupMode.COPY_EDITORS }); assert.equal(rightGroup.count, 1); - assert.ok(rightGroup.activeEditor instanceof TestEditorInput); + assert.ok(rightGroup.activeEditor instanceof TestFileEditorInput); part.mergeGroup(rootGroup, rightGroup, { mode: MergeGroupMode.MOVE_EDITORS }); assert.equal(rootGroup.count, 0); part.mergeGroup(rootGroup, downGroup); @@ -437,8 +371,8 @@ suite('EditorGroupsService', () => { editorWillCloseCounter++; }); - const input = new TestEditorInput(URI.file('foo/bar')); - const inputInactive = new TestEditorInput(URI.file('foo/bar/inactive')); + const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); await group.openEditor(input, EditorOptions.create({ pinned: true })); await group.openEditor(inputInactive, EditorOptions.create({ inactive: true })); @@ -465,7 +399,7 @@ suite('EditorGroupsService', () => { assert.ok(!group.previewEditor); assert.equal(group.activeEditor, input); - assert.ok(group.activeControl instanceof TestEditorControl); + assert.equal(group.activeControl?.getId(), TEST_EDITOR_ID); assert.equal(group.count, 2); const mru = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE); @@ -498,8 +432,8 @@ suite('EditorGroupsService', () => { const group = part.activeGroup; assert.equal(group.isEmpty, true); - const input = new TestEditorInput(URI.file('foo/bar')); - const inputInactive = new TestEditorInput(URI.file('foo/bar/inactive')); + const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); await group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]); assert.equal(group.count, 2); @@ -516,9 +450,9 @@ suite('EditorGroupsService', () => { const group = part.activeGroup; assert.equal(group.isEmpty, true); - const input1 = new TestEditorInput(URI.file('foo/bar1')); - const input2 = new TestEditorInput(URI.file('foo/bar2')); - const input3 = new TestEditorInput(URI.file('foo/bar3')); + const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); await group.openEditors([{ editor: input1, options: { pinned: true } }, { editor: input2, options: { pinned: true } }, { editor: input3 }]); assert.equal(group.count, 3); @@ -537,9 +471,9 @@ suite('EditorGroupsService', () => { const group = part.activeGroup; assert.equal(group.isEmpty, true); - const input1 = new TestEditorInput(URI.file('foo/bar1')); - const input2 = new TestEditorInput(URI.file('foo/bar2')); - const input3 = new TestEditorInput(URI.file('foo/bar3')); + const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); await group.openEditors([{ editor: input1, options: { pinned: true } }, { editor: input2, options: { pinned: true } }, { editor: input3 }]); assert.equal(group.count, 3); @@ -557,9 +491,9 @@ suite('EditorGroupsService', () => { const group = part.activeGroup; assert.equal(group.isEmpty, true); - const input1 = new TestEditorInput(URI.file('foo/bar1')); - const input2 = new TestEditorInput(URI.file('foo/bar2')); - const input3 = new TestEditorInput(URI.file('foo/bar3')); + const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); await group.openEditors([{ editor: input1, options: { pinned: true } }, { editor: input2, options: { pinned: true } }, { editor: input3 }]); assert.equal(group.count, 3); @@ -579,9 +513,9 @@ suite('EditorGroupsService', () => { const group = part.activeGroup; assert.equal(group.isEmpty, true); - const input1 = new TestEditorInput(URI.file('foo/bar1')); - const input2 = new TestEditorInput(URI.file('foo/bar2')); - const input3 = new TestEditorInput(URI.file('foo/bar3')); + const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); await group.openEditors([{ editor: input1, options: { pinned: true } }, { editor: input2, options: { pinned: true } }, { editor: input3 }]); assert.equal(group.count, 3); @@ -601,8 +535,8 @@ suite('EditorGroupsService', () => { const group = part.activeGroup; assert.equal(group.isEmpty, true); - const input = new TestEditorInput(URI.file('foo/bar')); - const inputInactive = new TestEditorInput(URI.file('foo/bar/inactive')); + const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); await group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]); assert.equal(group.count, 2); @@ -619,8 +553,8 @@ suite('EditorGroupsService', () => { const group = part.activeGroup; assert.equal(group.isEmpty, true); - const input = new TestEditorInput(URI.file('foo/bar')); - const inputInactive = new TestEditorInput(URI.file('foo/bar/inactive')); + const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); let editorMoveCounter = 0; const editorGroupChangeListener = group.onDidGroupChange(e => { @@ -649,8 +583,8 @@ suite('EditorGroupsService', () => { const rightGroup = part.addGroup(group, GroupDirection.RIGHT); - const input = new TestEditorInput(URI.file('foo/bar')); - const inputInactive = new TestEditorInput(URI.file('foo/bar/inactive')); + const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); await group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]); assert.equal(group.count, 2); @@ -671,8 +605,8 @@ suite('EditorGroupsService', () => { const rightGroup = part.addGroup(group, GroupDirection.RIGHT); - const input = new TestEditorInput(URI.file('foo/bar')); - const inputInactive = new TestEditorInput(URI.file('foo/bar/inactive')); + const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); await group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]); assert.equal(group.count, 2); @@ -692,8 +626,8 @@ suite('EditorGroupsService', () => { const group = part.activeGroup; assert.equal(group.isEmpty, true); - const input = new TestEditorInput(URI.file('foo/bar')); - const inputInactive = new TestEditorInput(URI.file('foo/bar/inactive')); + const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); await group.openEditor(input); assert.equal(group.count, 1); diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index f3b9f8dd63a..a57f108b8fb 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -4,21 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { EditorActivation, IEditorModel } from 'vs/platform/editor/common/editor'; +import { EditorActivation } from 'vs/platform/editor/common/editor'; import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { EditorInput, EditorOptions, IFileEditorInput, GroupIdentifier, ISaveOptions, IRevertOptions, EditorsOrder, IEditorInput, IMoveResult } from 'vs/workbench/common/editor'; -import { workbenchInstantiationService, TestStorageService, TestFileService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { EditorInput, EditorsOrder } from 'vs/workbench/common/editor'; +import { workbenchInstantiationService, TestStorageService, TestServiceAccessor, registerTestEditor, TestFileEditorInput } from 'vs/workbench/test/browser/workbenchTestServices'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { EditorService, DelegatingEditorService } from 'vs/workbench/services/editor/browser/editorService'; import { IEditorGroup, IEditorGroupsService, GroupDirection, GroupsArrangement } from 'vs/workbench/services/editor/common/editorGroupsService'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorRegistry, EditorDescriptor, Extensions } from 'vs/workbench/browser/editor'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { Registry } from 'vs/platform/registry/common/platform'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { timeout } from 'vs/base/common/async'; @@ -28,85 +26,10 @@ import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry'; import { UntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; import { NullFileSystemProvider } from 'vs/platform/files/test/common/nullFileSystemProvider'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; const TEST_EDITOR_ID = 'MyTestEditorForEditorService'; const TEST_EDITOR_INPUT_ID = 'testEditorInputForEditorService'; -class ServicesAccessor { - constructor( - @IFileService public fileService: TestFileService - ) { } -} - -class TestEditorControl extends BaseEditor { - - constructor(@ITelemetryService telemetryService: ITelemetryService) { super(TEST_EDITOR_ID, NullTelemetryService, new TestThemeService(), new TestStorageService()); } - - async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { - super.setInput(input, options, token); - - await input.resolve(); - } - - getId(): string { return TEST_EDITOR_ID; } - layout(): void { } - createEditor(): any { } -} - -class TestEditorInput extends EditorInput implements IFileEditorInput { - gotDisposed = false; - gotSaved = false; - gotSavedAs = false; - gotReverted = false; - dirty = false; - private fails = false; - constructor(public resource: URI) { super(); } - - getTypeId() { return TEST_EDITOR_INPUT_ID; } - resolve(): Promise { return !this.fails ? Promise.resolve(null) : Promise.reject(new Error('fails')); } - matches(other: TestEditorInput): boolean { return other && other.resource && this.resource.toString() === other.resource.toString() && other instanceof TestEditorInput; } - setEncoding(encoding: string) { } - getEncoding() { return undefined; } - setPreferredEncoding(encoding: string) { } - setMode(mode: string) { } - setPreferredMode(mode: string) { } - setForceOpenAsBinary(): void { } - setFailToOpen(): void { - this.fails = true; - } - async save(groupId: GroupIdentifier, options?: ISaveOptions): Promise { - this.gotSaved = true; - return this; - } - async saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise { - this.gotSavedAs = true; - return this; - } - async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { - this.gotReverted = true; - this.gotSaved = false; - this.gotSavedAs = false; - return true; - } - isDirty(): boolean { - return this.dirty; - } - isReadonly(): boolean { - return false; - } - isResolved(): boolean { return false; } - dispose(): void { - super.dispose(); - this.gotDisposed = true; - } - movedEditor: IMoveResult | undefined = undefined; - move(): IMoveResult | undefined { return this.movedEditor; } -} - class FileServiceProvider extends Disposable { constructor(scheme: string, @IFileService fileService: IFileService) { super(); @@ -120,7 +43,7 @@ suite('EditorService', () => { let disposables: IDisposable[] = []; setup(() => { - disposables.push(Registry.as(Extensions.Editors).registerEditor(EditorDescriptor.create(TestEditorControl, TEST_EDITOR_ID, 'My Test Editor For Next Editor Service'), [new SyncDescriptor(TestEditorInput)])); + disposables.push(registerTestEditor(TEST_EDITOR_ID, [new SyncDescriptor(TestFileEditorInput)], TEST_EDITOR_INPUT_ID)); }); teardown(() => { @@ -128,7 +51,7 @@ suite('EditorService', () => { disposables = []; }); - function createEditorService(): [EditorPart, EditorService, IInstantiationService, ServicesAccessor] { + function createEditorService(): [EditorPart, EditorService, TestServiceAccessor] { const instantiationService = workbenchInstantiationService(); const part = instantiationService.createInstance(EditorPart); @@ -140,14 +63,14 @@ suite('EditorService', () => { const editorService = instantiationService.createInstance(EditorService); instantiationService.stub(IEditorService, editorService); - return [part, editorService, instantiationService, instantiationService.createInstance(ServicesAccessor)]; + return [part, editorService, instantiationService.createInstance(TestServiceAccessor)]; } test('basics', async () => { - const [part, service, testInstantiationService] = createEditorService(); + const [part, service] = createEditorService(); - let input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-basics')); - let otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-basics')); + let input = new TestFileEditorInput(URI.parse('my://resource-basics'), TEST_EDITOR_INPUT_ID); + let otherInput = new TestFileEditorInput(URI.parse('my://resource2-basics'), TEST_EDITOR_INPUT_ID); let activeEditorChangeEventCounter = 0; const activeEditorChangeListener = service.onDidActiveEditorChange(() => { @@ -169,7 +92,7 @@ suite('EditorService', () => { // Open input let editor = await service.openEditor(input, { pinned: true }); - assert.ok(editor instanceof TestEditorControl); + assert.equal(editor?.getId(), TEST_EDITOR_ID); assert.equal(editor, service.activeControl); assert.equal(1, service.count); assert.equal(input, service.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[0].editor); @@ -200,8 +123,8 @@ suite('EditorService', () => { assert.equal(0, service.count); // Open again 2 inputs (recreate because disposed) - input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-basics')); - otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-basics')); + input = new TestFileEditorInput(URI.parse('my://resource-basics'), TEST_EDITOR_INPUT_ID); + otherInput = new TestFileEditorInput(URI.parse('my://resource2-basics'), TEST_EDITOR_INPUT_ID); await service.openEditor(input, { pinned: true }); editor = await service.openEditor(otherInput, { pinned: true }); @@ -226,11 +149,11 @@ suite('EditorService', () => { }); test('openEditors() / replaceEditors()', async () => { - const [part, service, testInstantiationService] = createEditorService(); + const [part, service] = createEditorService(); - const input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-openEditors')); - const otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-openEditors')); - const replaceInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource3-openEditors')); + const input = new TestFileEditorInput(URI.parse('my://resource-openEditors'), TEST_EDITOR_INPUT_ID); + const otherInput = new TestFileEditorInput(URI.parse('my://resource2-openEditors'), TEST_EDITOR_INPUT_ID); + const replaceInput = new TestFileEditorInput(URI.parse('my://resource3-openEditors'), TEST_EDITOR_INPUT_ID); await part.whenRestored; @@ -401,9 +324,9 @@ suite('EditorService', () => { }); test('close editor does not dispose when editor opened in other group', async () => { - const [part, service, testInstantiationService] = createEditorService(); + const [part, service] = createEditorService(); - const input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-close1')); + const input = new TestFileEditorInput(URI.parse('my://resource-close1'), TEST_EDITOR_INPUT_ID); const rootGroup = part.activeGroup; const rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); @@ -430,10 +353,10 @@ suite('EditorService', () => { }); test('open to the side', async () => { - const [part, service, testInstantiationService] = createEditorService(); + const [part, service] = createEditorService(); - const input1 = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource1-openside')); - const input2 = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-openside')); + const input1 = new TestFileEditorInput(URI.parse('my://resource1-openside'), TEST_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.parse('my://resource2-openside'), TEST_EDITOR_INPUT_ID); const rootGroup = part.activeGroup; @@ -456,10 +379,10 @@ suite('EditorService', () => { }); test('editor group activation', async () => { - const [part, service, testInstantiationService] = createEditorService(); + const [part, service] = createEditorService(); - const input1 = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource1-openside')); - const input2 = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-openside')); + const input1 = new TestFileEditorInput(URI.parse('my://resource1-openside'), TEST_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.parse('my://resource2-openside'), TEST_EDITOR_INPUT_ID); const rootGroup = part.activeGroup; @@ -491,10 +414,10 @@ suite('EditorService', () => { }); test('active editor change / visible editor change events', async function () { - const [part, service, testInstantiationService] = createEditorService(); + const [part, service] = createEditorService(); - let input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-active')); - let otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-active')); + let input = new TestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); + let otherInput = new TestFileEditorInput(URI.parse('my://resource2-active'), TEST_EDITOR_INPUT_ID); let activeEditorChangeEventFired = false; const activeEditorChangeListener = service.onDidActiveEditorChange(() => { @@ -546,8 +469,8 @@ suite('EditorService', () => { assertVisibleEditorsChangedEvent(true); // 2.) open, open same (forced open) (recreate inputs that got disposed) - input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-active')); - otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-active')); + input = new TestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); + otherInput = new TestFileEditorInput(URI.parse('my://resource2-active'), TEST_EDITOR_INPUT_ID); editor = await service.openEditor(input); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); @@ -559,8 +482,8 @@ suite('EditorService', () => { await closeEditorAndWaitForNextToOpen(group, input); // 3.) open, open inactive, close (recreate inputs that got disposed) - input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-active')); - otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-active')); + input = new TestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); + otherInput = new TestFileEditorInput(URI.parse('my://resource2-active'), TEST_EDITOR_INPUT_ID); editor = await service.openEditor(input, { pinned: true }); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); @@ -574,8 +497,8 @@ suite('EditorService', () => { assertVisibleEditorsChangedEvent(true); // 4.) open, open inactive, close inactive (recreate inputs that got disposed) - input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-active')); - otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-active')); + input = new TestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); + otherInput = new TestFileEditorInput(URI.parse('my://resource2-active'), TEST_EDITOR_INPUT_ID); editor = await service.openEditor(input, { pinned: true }); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); @@ -593,8 +516,8 @@ suite('EditorService', () => { assertVisibleEditorsChangedEvent(true); // 5.) add group, remove group (recreate inputs that got disposed) - input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-active')); - otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-active')); + input = new TestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); + otherInput = new TestFileEditorInput(URI.parse('my://resource2-active'), TEST_EDITOR_INPUT_ID); editor = await service.openEditor(input, { pinned: true }); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); @@ -616,8 +539,8 @@ suite('EditorService', () => { assertVisibleEditorsChangedEvent(true); // 6.) open editor in inactive group (recreate inputs that got disposed) - input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-active')); - otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-active')); + input = new TestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); + otherInput = new TestFileEditorInput(URI.parse('my://resource2-active'), TEST_EDITOR_INPUT_ID); editor = await service.openEditor(input, { pinned: true }); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); @@ -639,8 +562,8 @@ suite('EditorService', () => { assertVisibleEditorsChangedEvent(true); // 7.) activate group (recreate inputs that got disposed) - input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-active')); - otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-active')); + input = new TestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); + otherInput = new TestFileEditorInput(URI.parse('my://resource2-active'), TEST_EDITOR_INPUT_ID); editor = await service.openEditor(input, { pinned: true }); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); @@ -666,8 +589,8 @@ suite('EditorService', () => { assertVisibleEditorsChangedEvent(true); // 8.) move editor (recreate inputs that got disposed) - input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-active')); - otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-active')); + input = new TestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); + otherInput = new TestFileEditorInput(URI.parse('my://resource2-active'), TEST_EDITOR_INPUT_ID); editor = await service.openEditor(input, { pinned: true }); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); @@ -685,8 +608,8 @@ suite('EditorService', () => { assertVisibleEditorsChangedEvent(true); // 9.) close editor in inactive group (recreate inputs that got disposed) - input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-active')); - otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-active')); + input = new TestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); + otherInput = new TestFileEditorInput(URI.parse('my://resource2-active'), TEST_EDITOR_INPUT_ID); editor = await service.openEditor(input, { pinned: true }); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); @@ -711,9 +634,9 @@ suite('EditorService', () => { }); test('two active editor change events when opening editor to the side', async function () { - const [part, service, testInstantiationService] = createEditorService(); + const [part, service] = createEditorService(); - let input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-active')); + let input = new TestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); let activeEditorChangeEvents = 0; const activeEditorChangeListener = service.onDidActiveEditorChange(() => { @@ -763,11 +686,11 @@ suite('EditorService', () => { }); test('openEditor returns NULL when opening fails or is inactive', async function () { - const [part, service, testInstantiationService] = createEditorService(); + const [part, service] = createEditorService(); - const input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-active')); - const otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-inactive')); - const failingInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource3-failing')); + const input = new TestFileEditorInput(URI.parse('my://resource-active'), TEST_EDITOR_INPUT_ID); + const otherInput = new TestFileEditorInput(URI.parse('my://resource2-inactive'), TEST_EDITOR_INPUT_ID); + const failingInput = new TestFileEditorInput(URI.parse('my://resource3-failing'), TEST_EDITOR_INPUT_ID); failingInput.setFailToOpen(); await part.whenRestored; @@ -785,11 +708,11 @@ suite('EditorService', () => { }); test('save, saveAll, revertAll', async function () { - const [part, service, testInstantiationService] = createEditorService(); + const [part, service] = createEditorService(); - const input1 = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource1-openside')); + const input1 = new TestFileEditorInput(URI.parse('my://resource1-openside'), TEST_EDITOR_INPUT_ID); input1.dirty = true; - const input2 = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-openside')); + const input2 = new TestFileEditorInput(URI.parse('my://resource2-openside'), TEST_EDITOR_INPUT_ID); input2.dirty = true; const rootGroup = part.activeGroup; @@ -828,11 +751,11 @@ suite('EditorService', () => { }); async function testFileDeleteEditorClose(dirty: boolean): Promise { - const [part, service, testInstantiationService, accessor] = createEditorService(); + const [part, service, accessor] = createEditorService(); - const input1 = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource1-openside')); + const input1 = new TestFileEditorInput(URI.parse('my://resource1-openside'), TEST_EDITOR_INPUT_ID); input1.dirty = dirty; - const input2 = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-openside')); + const input2 = new TestFileEditorInput(URI.parse('my://resource2-openside'), TEST_EDITOR_INPUT_ID); input2.dirty = dirty; const rootGroup = part.activeGroup; @@ -860,10 +783,10 @@ suite('EditorService', () => { } test('file move asks input to move', async function () { - const [part, service, testInstantiationService, accessor] = createEditorService(); + const [part, service, accessor] = createEditorService(); - const input1 = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource1-openside')); - const movedInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-openside')); + const input1 = new TestFileEditorInput(URI.parse('my://resource1-openside'), TEST_EDITOR_INPUT_ID); + const movedInput = new TestFileEditorInput(URI.parse('my://resource2-openside'), TEST_EDITOR_INPUT_ID); input1.movedEditor = { editor: movedInput }; const rootGroup = part.activeGroup; @@ -896,4 +819,26 @@ suite('EditorService', () => { Event.once(editorService.onDidActiveEditorChange)(c); }); } + + test('file watcher gets installed for out of workspace files', async function () { + const [part, service, accessor] = createEditorService(); + + const input1 = new TestFileEditorInput(URI.parse('file://resource1-openside'), TEST_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.parse('file://resource2-openside'), TEST_EDITOR_INPUT_ID); + + await part.whenRestored; + + await service.openEditor(input1, { pinned: true }); + assert.equal(accessor.fileService.watches.length, 1); + assert.equal(accessor.fileService.watches[0].toString(), input1.resource.toString()); + + const editor = await service.openEditor(input2, { pinned: true }); + assert.equal(accessor.fileService.watches.length, 1); + assert.equal(accessor.fileService.watches[0].toString(), input2.resource.toString()); + + await editor?.group?.closeAllEditors(); + assert.equal(accessor.fileService.watches.length, 0); + + part.dispose(); + }); }); diff --git a/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts b/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts index a13db24ac3e..2f25fec289d 100644 --- a/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts @@ -4,21 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { EditorOptions, EditorInput, IEditorInputFactoryRegistry, Extensions as EditorExtensions, IEditorInputFactory, IFileEditorInput } from 'vs/workbench/common/editor'; +import { EditorOptions, IEditorInputFactoryRegistry, Extensions as EditorExtensions } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; -import { workbenchInstantiationService, TestStorageService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { workbenchInstantiationService, TestStorageService, TestFileEditorInput, registerTestEditor } from 'vs/workbench/test/browser/workbenchTestServices'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; -import { IEditorRegistry, EditorDescriptor, Extensions } from 'vs/workbench/browser/editor'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { GroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { EditorActivation, IEditorModel } from 'vs/platform/editor/common/editor'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; -import { CancellationToken } from 'vs/base/common/cancellation'; +import { EditorActivation } from 'vs/platform/editor/common/editor'; import { WillSaveStateReason } from 'vs/platform/storage/common/storage'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { EditorsObserver } from 'vs/workbench/browser/parts/editor/editorsObserver'; import { timeout } from 'vs/base/common/async'; @@ -27,78 +21,12 @@ const TEST_EDITOR_ID = 'MyTestEditorForEditorsObserver'; const TEST_EDITOR_INPUT_ID = 'testEditorInputForEditorsObserver'; const TEST_SERIALIZABLE_EDITOR_INPUT_ID = 'testSerializableEditorInputForEditorsObserver'; -class TestEditorControl extends BaseEditor { - - constructor() { super(TEST_EDITOR_ID, NullTelemetryService, new TestThemeService(), new TestStorageService()); } - - async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { - super.setInput(input, options, token); - - await input.resolve(); - } - - getId(): string { return TEST_EDITOR_ID; } - layout(): void { } - createEditor(): any { } -} - -class TestEditorInput extends EditorInput implements IFileEditorInput { - - private dirty = false; - - constructor(public resource: URI) { super(); } - - getTypeId() { return TEST_EDITOR_INPUT_ID; } - resolve(): Promise { return Promise.resolve(null); } - matches(other: TestEditorInput): boolean { return other && this.resource.toString() === other.resource.toString() && other instanceof TestEditorInput; } - setEncoding(encoding: string) { } - getEncoding() { return undefined; } - setPreferredEncoding(encoding: string) { } - setMode(mode: string) { } - setPreferredMode(mode: string) { } - setForceOpenAsBinary(): void { } - isDirty(): boolean { return this.dirty; } - setDirty(): void { this.dirty = true; } - isResolved(): boolean { return false; } -} - -class EditorsObserverTestEditorInput extends TestEditorInput { - getTypeId() { return TEST_SERIALIZABLE_EDITOR_INPUT_ID; } -} - -interface ISerializedTestInput { - resource: string; -} - -class EditorsObserverTestEditorInputFactory implements IEditorInputFactory { - - canSerialize(editorInput: EditorInput): boolean { - return true; - } - - serialize(editorInput: EditorInput): string { - let testEditorInput = editorInput; - let testInput: ISerializedTestInput = { - resource: testEditorInput.resource.toString() - }; - - return JSON.stringify(testInput); - } - - deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput { - let testInput: ISerializedTestInput = JSON.parse(serializedEditorInput); - - return new EditorsObserverTestEditorInput(URI.parse(testInput.resource)); - } -} - suite('EditorsObserver', function () { let disposables: IDisposable[] = []; setup(() => { - disposables.push(Registry.as(EditorExtensions.EditorInputFactories).registerEditorInputFactory(TEST_SERIALIZABLE_EDITOR_INPUT_ID, EditorsObserverTestEditorInputFactory)); - disposables.push(Registry.as(Extensions.Editors).registerEditor(EditorDescriptor.create(TestEditorControl, TEST_EDITOR_ID, 'My Test Editor For Editors Observer'), [new SyncDescriptor(TestEditorInput), new SyncDescriptor(EditorsObserverTestEditorInput)])); + disposables.push(registerTestEditor(TEST_EDITOR_ID, [new SyncDescriptor(TestFileEditorInput)], TEST_SERIALIZABLE_EDITOR_INPUT_ID)); }); teardown(() => { @@ -139,7 +67,7 @@ suite('EditorsObserver', function () { assert.equal(currentEditorsMRU.length, 0); assert.equal(observerChangeListenerCalled, false); - const input1 = new EditorsObserverTestEditorInput(URI.parse('foo://bar1')); + const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true })); @@ -149,8 +77,8 @@ suite('EditorsObserver', function () { assert.equal(currentEditorsMRU[0].editor, input1); assert.equal(observerChangeListenerCalled, true); - const input2 = new EditorsObserverTestEditorInput(URI.parse('foo://bar2')); - const input3 = new EditorsObserverTestEditorInput(URI.parse('foo://bar3')); + const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); + const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true })); await part.activeGroup.openEditor(input3, EditorOptions.create({ pinned: true })); @@ -204,7 +132,7 @@ suite('EditorsObserver', function () { const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); - const input1 = new EditorsObserverTestEditorInput(URI.parse('foo://bar1')); + const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true, activation: EditorActivation.ACTIVATE })); await sideGroup.openEditor(input1, EditorOptions.create({ pinned: true, activation: EditorActivation.ACTIVATE })); @@ -227,7 +155,7 @@ suite('EditorsObserver', function () { // Opening an editor inactive should not change // the most recent editor, but rather put it behind - const input2 = new EditorsObserverTestEditorInput(URI.parse('foo://bar2')); + const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); await rootGroup.openEditor(input2, EditorOptions.create({ inactive: true })); @@ -258,9 +186,9 @@ suite('EditorsObserver', function () { test('copy group', async () => { const [part, observer] = await createEditorObserver(); - const input1 = new EditorsObserverTestEditorInput(URI.parse('foo://bar1')); - const input2 = new EditorsObserverTestEditorInput(URI.parse('foo://bar2')); - const input3 = new EditorsObserverTestEditorInput(URI.parse('foo://bar3')); + const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); + const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); const rootGroup = part.activeGroup; @@ -303,9 +231,9 @@ suite('EditorsObserver', function () { const rootGroup = part.activeGroup; - const input1 = new EditorsObserverTestEditorInput(URI.parse('foo://bar1')); - const input2 = new EditorsObserverTestEditorInput(URI.parse('foo://bar2')); - const input3 = new EditorsObserverTestEditorInput(URI.parse('foo://bar3')); + const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); + const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true })); await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true })); @@ -346,9 +274,9 @@ suite('EditorsObserver', function () { const rootGroup = part.activeGroup; - const input1 = new EditorsObserverTestEditorInput(URI.parse('foo://bar1')); - const input2 = new EditorsObserverTestEditorInput(URI.parse('foo://bar2')); - const input3 = new EditorsObserverTestEditorInput(URI.parse('foo://bar3')); + const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); + const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true })); await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true })); @@ -391,7 +319,7 @@ suite('EditorsObserver', function () { const rootGroup = part.activeGroup; - const input1 = new TestEditorInput(URI.parse('foo://bar1')); + const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true })); @@ -425,10 +353,10 @@ suite('EditorsObserver', function () { const rootGroup = part.activeGroup; const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); - const input1 = new TestEditorInput(URI.parse('foo://bar1')); - const input2 = new TestEditorInput(URI.parse('foo://bar2')); - const input3 = new TestEditorInput(URI.parse('foo://bar3')); - const input4 = new TestEditorInput(URI.parse('foo://bar4')); + const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID); + const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_EDITOR_INPUT_ID); + const input4 = new TestFileEditorInput(URI.parse('foo://bar4'), TEST_EDITOR_INPUT_ID); await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true })); await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true })); @@ -452,7 +380,7 @@ suite('EditorsObserver', function () { assert.equal(rootGroup.isOpened(input3), false); assert.equal(rootGroup.isOpened(input4), true); - const input5 = new TestEditorInput(URI.parse('foo://bar5')); + const input5 = new TestFileEditorInput(URI.parse('foo://bar5'), TEST_EDITOR_INPUT_ID); await sideGroup.openEditor(input5, EditorOptions.create({ pinned: true })); assert.equal(rootGroup.count, 1); @@ -477,10 +405,10 @@ suite('EditorsObserver', function () { const rootGroup = part.activeGroup; const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); - const input1 = new TestEditorInput(URI.parse('foo://bar1')); - const input2 = new TestEditorInput(URI.parse('foo://bar2')); - const input3 = new TestEditorInput(URI.parse('foo://bar3')); - const input4 = new TestEditorInput(URI.parse('foo://bar4')); + const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID); + const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_EDITOR_INPUT_ID); + const input4 = new TestFileEditorInput(URI.parse('foo://bar4'), TEST_EDITOR_INPUT_ID); await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true })); await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true })); diff --git a/src/vs/workbench/services/history/browser/history.ts b/src/vs/workbench/services/history/browser/history.ts index 667422f2e44..8f081c5e22e 100644 --- a/src/vs/workbench/services/history/browser/history.ts +++ b/src/vs/workbench/services/history/browser/history.ts @@ -34,7 +34,6 @@ import { addDisposableListener, EventType, EventHelper } from 'vs/base/browser/d import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; import { Schemas } from 'vs/base/common/network'; import { isEqual } from 'vs/base/common/resources'; -import { ILogService } from 'vs/platform/log/common/log'; /** * Stores the selection & view state of an editor and allows to compare it to other selection states. @@ -112,8 +111,7 @@ export class HistoryService extends Disposable implements IHistoryService { @IWorkspacesService private readonly workspacesService: IWorkspacesService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @ILogService private readonly logService: ILogService + @IContextKeyService private readonly contextKeyService: IContextKeyService ) { super(); @@ -277,7 +275,9 @@ export class HistoryService extends Disposable implements IHistoryService { } clear(): void { - this.ensureHistoryLoaded(); + + // History + this.clearRecentlyOpened(); // Navigation (next, previous) this.navigationStackIndex = -1; @@ -289,9 +289,6 @@ export class HistoryService extends Disposable implements IHistoryService { // Closed files this.recentlyClosedFiles = []; - // History - this.clearRecentlyOpened(); - // Context Keys this.updateContextKeys(); } @@ -719,8 +716,8 @@ export class HistoryService extends Disposable implements IHistoryService { private static readonly MAX_HISTORY_ITEMS = 200; private static readonly HISTORY_STORAGE_KEY = 'history.entries'; - private history: Array = []; - private loaded = false; + private history: Array | undefined = undefined; + private readonly resourceFilter = this._register(this.instantiationService.createInstance( ResourceGlobMatcher, (root?: URI) => this.getExcludes(root), @@ -741,11 +738,10 @@ export class HistoryService extends Disposable implements IHistoryService { return; } - this.ensureHistoryLoaded(); - const historyInput = this.preferResourceInput(input); // Remove any existing entry and add to the beginning + this.ensureHistoryLoaded(this.history); this.removeFromHistory(input); this.history.unshift(historyInput); @@ -772,7 +768,7 @@ export class HistoryService extends Disposable implements IHistoryService { } private removeExcludedFromHistory(): void { - this.ensureHistoryLoaded(); + this.ensureHistoryLoaded(this.history); this.history = this.history.filter(e => { const include = this.include(e); @@ -787,7 +783,7 @@ export class HistoryService extends Disposable implements IHistoryService { } private removeFromHistory(arg1: IEditorInput | IResourceInput | FileChangesEvent): void { - this.ensureHistoryLoaded(); + this.ensureHistoryLoaded(this.history); this.history = this.history.filter(e => { const matches = this.matches(arg1, e); @@ -809,17 +805,59 @@ export class HistoryService extends Disposable implements IHistoryService { } getHistory(): ReadonlyArray { - this.ensureHistoryLoaded(); + this.ensureHistoryLoaded(this.history); return this.history.slice(0); } - private ensureHistoryLoaded(): void { - if (!this.loaded) { - this.loadHistory(); + private ensureHistoryLoaded(history: Array | undefined): asserts history { + if (!this.history) { + this.history = this.loadHistory(); + } + } + + private loadHistory(): Array { + let entries: ISerializedEditorHistoryEntry[] = []; + + const entriesRaw = this.storageService.get(HistoryService.HISTORY_STORAGE_KEY, StorageScope.WORKSPACE); + if (entriesRaw) { + entries = coalesce(JSON.parse(entriesRaw)); } - this.loaded = true; + const registry = Registry.as(EditorExtensions.EditorInputFactories); + + return coalesce(entries.map(entry => { + try { + return this.safeLoadHistoryEntry(registry, entry); + } catch (error) { + return undefined; // https://github.com/Microsoft/vscode/issues/60960 + } + })); + } + + private safeLoadHistoryEntry(registry: IEditorInputFactoryRegistry, entry: ISerializedEditorHistoryEntry): IEditorInput | IResourceInput | undefined { + const serializedEditorHistoryEntry = entry; + + // File resource: via URI.revive() + if (serializedEditorHistoryEntry.resourceJSON) { + return { resource: URI.revive(serializedEditorHistoryEntry.resourceJSON) }; + } + + // Editor input: via factory + const { editorInputJSON } = serializedEditorHistoryEntry; + if (editorInputJSON?.deserialized) { + const factory = registry.getEditorInputFactory(editorInputJSON.typeId); + if (factory) { + const input = factory.deserialize(this.instantiationService, editorInputJSON.deserialized); + if (input) { + this.onEditorDispose(input, () => this.removeFromHistory(input), this.editorHistoryListeners); + } + + return withNullAsUndefined(input); + } + } + + return undefined; } private saveState(): void { @@ -850,58 +888,9 @@ export class HistoryService extends Disposable implements IHistoryService { return undefined; })); - this.logService.trace(`[editor history] saving ${entries.length} entries`); this.storageService.store(HistoryService.HISTORY_STORAGE_KEY, JSON.stringify(entries), StorageScope.WORKSPACE); } - private loadHistory(): void { - let entries: ISerializedEditorHistoryEntry[] = []; - - const entriesRaw = this.storageService.get(HistoryService.HISTORY_STORAGE_KEY, StorageScope.WORKSPACE); - if (entriesRaw) { - entries = coalesce(JSON.parse(entriesRaw)); - } - - const registry = Registry.as(EditorExtensions.EditorInputFactories); - - this.history = coalesce(entries.map(entry => { - try { - return this.safeLoadHistoryEntry(registry, entry); - } catch (error) { - this.logService.error(`[editor history] error loading one editor history entry: ${error.toString()}`); - - return undefined; // https://github.com/Microsoft/vscode/issues/60960 - } - })); - - this.logService.trace(`[editor history] loading ${this.history.length} entries`); - } - - private safeLoadHistoryEntry(registry: IEditorInputFactoryRegistry, entry: ISerializedEditorHistoryEntry): IEditorInput | IResourceInput | undefined { - const serializedEditorHistoryEntry = entry; - - // File resource: via URI.revive() - if (serializedEditorHistoryEntry.resourceJSON) { - return { resource: URI.revive(serializedEditorHistoryEntry.resourceJSON) }; - } - - // Editor input: via factory - const { editorInputJSON } = serializedEditorHistoryEntry; - if (editorInputJSON?.deserialized) { - const factory = registry.getEditorInputFactory(editorInputJSON.typeId); - if (factory) { - const input = factory.deserialize(this.instantiationService, editorInputJSON.deserialized); - if (input) { - this.onEditorDispose(input, () => this.removeFromHistory(input), this.editorHistoryListeners); - } - - return withNullAsUndefined(input); - } - } - - return undefined; - } - //#endregion //#region Last Active Workspace/File @@ -925,8 +914,7 @@ export class HistoryService extends Disposable implements IHistoryService { } // Multiple folders: find the last active one - const history = this.getHistory(); - for (const input of history) { + for (const input of this.getHistory()) { if (input instanceof EditorInput) { continue; } @@ -954,8 +942,7 @@ export class HistoryService extends Disposable implements IHistoryService { } getLastActiveFile(filterByScheme: string): URI | undefined { - const history = this.getHistory(); - for (const input of history) { + for (const input of this.getHistory()) { let resource: URI | undefined; if (input instanceof EditorInput) { resource = toResource(input, { filterByScheme }); diff --git a/src/vs/workbench/services/history/test/browser/history.test.ts b/src/vs/workbench/services/history/test/browser/history.test.ts index 77715cbc34a..e2e5b97413a 100644 --- a/src/vs/workbench/services/history/test/browser/history.test.ts +++ b/src/vs/workbench/services/history/test/browser/history.test.ts @@ -4,21 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { EditorOptions, EditorInput, IEditorInputFactoryRegistry, Extensions as EditorExtensions, IEditorInputFactory, IFileEditorInput } from 'vs/workbench/common/editor'; +import { EditorOptions } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; -import { workbenchInstantiationService, TestStorageService } from 'vs/workbench/test/browser/workbenchTestServices'; -import { Registry } from 'vs/platform/registry/common/platform'; +import { workbenchInstantiationService, TestFileEditorInput, registerTestEditor } from 'vs/workbench/test/browser/workbenchTestServices'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; -import { IEditorRegistry, EditorDescriptor, Extensions } from 'vs/workbench/browser/editor'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IEditorGroupsService, GroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IEditorModel } from 'vs/platform/editor/common/editor'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; -import { CancellationToken } from 'vs/base/common/cancellation'; import { HistoryService } from 'vs/workbench/services/history/browser/history'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; @@ -27,68 +19,6 @@ import { timeout } from 'vs/base/common/async'; const TEST_EDITOR_ID = 'MyTestEditorForEditorHistory'; const TEST_EDITOR_INPUT_ID = 'testEditorInputForHistoyService'; -const TEST_SERIALIZABLE_EDITOR_INPUT_ID = 'testSerializableEditorInputForHistoyService'; - -class TestEditorControl extends BaseEditor { - - constructor() { super(TEST_EDITOR_ID, NullTelemetryService, new TestThemeService(), new TestStorageService()); } - - async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { - super.setInput(input, options, token); - - await input.resolve(); - } - - getId(): string { return TEST_EDITOR_ID; } - layout(): void { } - createEditor(): any { } -} - -class TestEditorInput extends EditorInput implements IFileEditorInput { - - constructor(public resource: URI) { super(); } - - getTypeId() { return TEST_EDITOR_INPUT_ID; } - resolve(): Promise { return Promise.resolve(null); } - matches(other: TestEditorInput): boolean { return other && this.resource.toString() === other.resource.toString() && other instanceof TestEditorInput; } - setEncoding(encoding: string) { } - getEncoding() { return undefined; } - setPreferredEncoding(encoding: string) { } - setMode(mode: string) { } - setPreferredMode(mode: string) { } - setForceOpenAsBinary(): void { } - isResolved(): boolean { return false; } -} - -class HistoryTestEditorInput extends TestEditorInput { - getTypeId() { return TEST_SERIALIZABLE_EDITOR_INPUT_ID; } -} - -interface ISerializedTestInput { - resource: string; -} - -class HistoryTestEditorInputFactory implements IEditorInputFactory { - - canSerialize(editorInput: EditorInput): boolean { - return true; - } - - serialize(editorInput: EditorInput): string { - let testEditorInput = editorInput; - let testInput: ISerializedTestInput = { - resource: testEditorInput.resource.toString() - }; - - return JSON.stringify(testInput); - } - - deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput { - let testInput: ISerializedTestInput = JSON.parse(serializedEditorInput); - - return new HistoryTestEditorInput(URI.parse(testInput.resource)); - } -} async function createServices(): Promise<[EditorPart, HistoryService, EditorService]> { const instantiationService = workbenchInstantiationService(); @@ -115,8 +45,7 @@ suite('HistoryService', function () { let disposables: IDisposable[] = []; setup(() => { - disposables.push(Registry.as(EditorExtensions.EditorInputFactories).registerEditorInputFactory(TEST_SERIALIZABLE_EDITOR_INPUT_ID, HistoryTestEditorInputFactory)); - disposables.push(Registry.as(Extensions.Editors).registerEditor(EditorDescriptor.create(TestEditorControl, TEST_EDITOR_ID, 'My Test Editor For History Editor Service'), [new SyncDescriptor(TestEditorInput), new SyncDescriptor(HistoryTestEditorInput)])); + disposables.push(registerTestEditor(TEST_EDITOR_ID, [new SyncDescriptor(TestFileEditorInput)])); }); teardown(() => { @@ -127,11 +56,11 @@ suite('HistoryService', function () { test('back / forward', async () => { const [part, historyService] = await createServices(); - const input1 = new TestEditorInput(URI.parse('foo://bar1')); + const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true })); assert.equal(part.activeGroup.activeEditor, input1); - const input2 = new TestEditorInput(URI.parse('foo://bar2')); + const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID); await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true })); assert.equal(part.activeGroup.activeEditor, input2); @@ -150,10 +79,10 @@ suite('HistoryService', function () { let history = historyService.getHistory(); assert.equal(history.length, 0); - const input1 = new TestEditorInput(URI.parse('foo://bar1')); + const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true })); - const input2 = new TestEditorInput(URI.parse('foo://bar2')); + const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID); await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true })); history = historyService.getHistory(); @@ -172,7 +101,7 @@ suite('HistoryService', function () { assert.ok(!historyService.getLastActiveFile('foo')); - const input1 = new TestEditorInput(URI.parse('foo://bar1')); + const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true })); assert.equal(historyService.getLastActiveFile('foo')?.toString(), input1.resource.toString()); @@ -183,8 +112,8 @@ suite('HistoryService', function () { test('open next/previous recently used editor (single group)', async () => { const [part, historyService] = await createServices(); - const input1 = new TestEditorInput(URI.parse('foo://bar1')); - const input2 = new TestEditorInput(URI.parse('foo://bar2')); + const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID); await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true })); assert.equal(part.activeGroup.activeEditor, input1); @@ -211,8 +140,8 @@ suite('HistoryService', function () { const [part, historyService] = await createServices(); const rootGroup = part.activeGroup; - const input1 = new TestEditorInput(URI.parse('foo://bar1')); - const input2 = new TestEditorInput(URI.parse('foo://bar2')); + const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID); const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); @@ -233,10 +162,10 @@ suite('HistoryService', function () { test('open next/previous recently is reset when other input opens', async () => { const [part, historyService] = await createServices(); - const input1 = new TestEditorInput(URI.parse('foo://bar1')); - const input2 = new TestEditorInput(URI.parse('foo://bar2')); - const input3 = new TestEditorInput(URI.parse('foo://bar3')); - const input4 = new TestEditorInput(URI.parse('foo://bar4')); + const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID); + const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_EDITOR_INPUT_ID); + const input4 = new TestFileEditorInput(URI.parse('foo://bar4'), TEST_EDITOR_INPUT_ID); await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true })); await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true })); diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index 296e4f47bf5..6d4723b34e3 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -233,7 +233,8 @@ export class ProgressService extends Disposable implements IProgressService { severity: Severity.Info, message, source: options.source, - actions: { primary: primaryActions, secondary: secondaryActions } + actions: { primary: primaryActions, secondary: secondaryActions }, + progress: typeof increment === 'number' && increment >= 0 ? { total: 100, worked: increment } : { infinite: true } }); updateProgress(handle, increment); diff --git a/src/vs/workbench/services/search/common/search.ts b/src/vs/workbench/services/search/common/search.ts index 99ba52ad15f..e646364a885 100644 --- a/src/vs/workbench/services/search/common/search.ts +++ b/src/vs/workbench/services/search/common/search.ts @@ -198,6 +198,12 @@ export interface ISearchCompleteStats { export interface ISearchComplete extends ISearchCompleteStats { results: IFileMatch[]; + exit?: SearchCompletionExitCode +} + +export const enum SearchCompletionExitCode { + Normal, + NewSearchStarted } export interface ITextSearchStats { @@ -334,9 +340,7 @@ export interface ISearchConfigurationProperties { collapseResults: 'auto' | 'alwaysCollapse' | 'alwaysExpand'; searchOnType: boolean; searchOnTypeDebouncePeriod: number; - enableSearchEditorPreview: boolean; - searchEditorPreview: { doubleClickBehaviour: 'selectWord' | 'goToLocation' | 'openLocationToSide' }; - searchEditorPreviewForceAbsolutePaths: boolean; + searchEditor: { doubleClickBehaviour: 'selectWord' | 'goToLocation' | 'openLocationToSide' }; sortOrder: SearchSortOrder; } diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 7afb0645e24..67332750e50 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -251,6 +251,13 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil async load(options?: ITextFileLoadOptions): Promise { this.logService.trace('[text file model] load() - enter', this.resource.toString(true)); + // Return early if we are disposed + if (this.isDisposed()) { + this.logService.trace('[text file model] load() - exit - without loading because model is disposed', this.resource.toString(true)); + + return this; + } + // It is very important to not reload the model when the model is dirty. // We also only want to reload the model from the disk if no save is pending // to avoid data loss. @@ -359,7 +366,14 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private loadFromContent(content: ITextFileStreamContent, options?: ITextFileLoadOptions, fromBackup?: boolean): TextFileEditorModel { - this.logService.trace('[text file model] load() - resolved content', this.resource.toString(true)); + this.logService.trace('[text file model] loadFromContent() - enter', this.resource.toString(true)); + + // Return early if we are disposed + if (this.isDisposed()) { + this.logService.trace('[text file model] loadFromContent() - exit - because model is disposed', this.resource.toString(true)); + + return this; + } // Update our resolved disk stat model this.updateLastResolvedFileStat({ @@ -405,7 +419,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private doCreateTextModel(resource: URI, value: ITextBufferFactory, fromBackup: boolean): void { - this.logService.trace('[text file model] load() - created text editor model', this.resource.toString(true)); + this.logService.trace('[text file model] doCreateTextModel()', this.resource.toString(true)); // Create model const textModel = this.createTextEditorModel(value, resource, this.preferredMode); @@ -420,7 +434,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private doUpdateTextModel(value: ITextBufferFactory): void { - this.logService.trace('[text file model] load() - updated text editor model', this.resource.toString(true)); + this.logService.trace('[text file model] doUpdateTextModel()', this.resource.toString(true)); // Update model value in a block that ignores content change events for dirty tracking this.ignoreDirtyOnModelContentChange = true; @@ -703,7 +717,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private handleSaveSuccess(stat: IFileStatWithMetadata, versionId: number, options: ITextFileSaveOptions): void { - this.logService.trace(`[text file model] doSave(${versionId}) - after write()`, this.resource.toString(true)); // Updated resolved stat with updated stat this.updateLastResolvedFileStat(stat); diff --git a/src/vs/workbench/services/textfile/common/textFileSaveParticipant.ts b/src/vs/workbench/services/textfile/common/textFileSaveParticipant.ts index eb9ce782052..0c40afacfa3 100644 --- a/src/vs/workbench/services/textfile/common/textFileSaveParticipant.ts +++ b/src/vs/workbench/services/textfile/common/textFileSaveParticipant.ts @@ -33,7 +33,7 @@ export class TextFileSaveParticipant extends Disposable { const cts = new CancellationTokenSource(token); return this.progressService.withProgress({ - title: localize('saveParticipants', "Running Save Participants for '{0}'", model.name), + title: localize('saveParticipants', "Saving '{0}'", model.name), location: ProgressLocation.Notification, cancellable: true, delay: model.isDirty() ? 3000 : 5000 diff --git a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts index f1847f3189d..7140b161af9 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts @@ -7,50 +7,31 @@ import * as assert from 'assert'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { EncodingMode } from 'vs/workbench/common/editor'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; -import { ITextFileService, TextFileEditorModelState, snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; -import { createFileInput, TestFileService, TestTextFileService, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TextFileEditorModelState, snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; +import { createFileInput, workbenchInstantiationService, TestServiceAccessor, TestReadonlyTextFileEditorModel } from 'vs/workbench/test/browser/workbenchTestServices'; import { toResource } from 'vs/base/test/common/utils'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; -import { FileOperationResult, FileOperationError, IFileService } from 'vs/platform/files/common/files'; -import { IModelService } from 'vs/editor/common/services/modelService'; +import { FileOperationResult, FileOperationError } from 'vs/platform/files/common/files'; import { timeout } from 'vs/base/common/async'; import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry'; import { assertIsDefined } from 'vs/base/common/types'; -import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; -class ServiceAccessor { - constructor( - @ITextFileService public readonly textFileService: TestTextFileService, - @IModelService public readonly modelService: IModelService, - @IFileService public readonly fileService: TestFileService, - @IWorkingCopyService public readonly workingCopyService: IWorkingCopyService - ) { - } -} - function getLastModifiedTime(model: TextFileEditorModel): number { const stat = model.getStat(); return stat ? stat.mtime : -1; } -class TestTextFileEditorModel extends TextFileEditorModel { - - isReadonly(): boolean { - return true; - } -} - suite('Files - TextFileEditorModel', () => { let instantiationService: IInstantiationService; - let accessor: ServiceAccessor; + let accessor: TestServiceAccessor; let content: string; setup(() => { instantiationService = workbenchInstantiationService(); - accessor = instantiationService.createInstance(ServiceAccessor); + accessor = instantiationService.createInstance(TestServiceAccessor); content = accessor.fileService.getContent(); }); @@ -107,7 +88,7 @@ suite('Files - TextFileEditorModel', () => { assert.equal(accessor.workingCopyService.isDirty(model.resource), true); let savedEvent = false; - model.onDidSave(e => savedEvent = true); + model.onDidSave(() => savedEvent = true); let workingCopyEvent = false; accessor.workingCopyService.onDidChangeDirty(e => { @@ -139,7 +120,7 @@ suite('Files - TextFileEditorModel', () => { await model.load(); let savedEvent = false; - model.onDidSave(e => savedEvent = true); + model.onDidSave(() => savedEvent = true); let workingCopyEvent = false; accessor.workingCopyService.onDidChangeDirty(e => { @@ -165,7 +146,7 @@ suite('Files - TextFileEditorModel', () => { model.updateTextEditorModel(createTextBufferFactory('bar')); let saveErrorEvent = false; - model.onDidSaveError(e => saveErrorEvent = true); + model.onDidSaveError(() => saveErrorEvent = true); accessor.fileService.writeShouldThrowError = new Error('failed to write'); try { @@ -195,7 +176,7 @@ suite('Files - TextFileEditorModel', () => { model.updateTextEditorModel(createTextBufferFactory('bar')); let saveErrorEvent = false; - model.onDidSaveError(e => saveErrorEvent = true); + model.onDidSaveError(() => saveErrorEvent = true); accessor.fileService.writeShouldThrowError = new FileOperationError('save conflict', FileOperationResult.FILE_MODIFIED_SINCE); try { @@ -221,7 +202,7 @@ suite('Files - TextFileEditorModel', () => { const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); let encodingEvent = false; - model.onDidChangeEncoding(e => encodingEvent = true); + model.onDidChangeEncoding(() => encodingEvent = true); model.setEncoding('utf8', EncodingMode.Encode); // no-op assert.equal(getLastModifiedTime(model), -1); @@ -276,8 +257,8 @@ suite('Files - TextFileEditorModel', () => { const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index.txt'), 'utf8', undefined); assert.ok(model.hasState(TextFileEditorModelState.SAVED)); - model.onDidSave(e => assert.fail()); - model.onDidChangeDirty(e => assert.fail()); + model.onDidSave(() => assert.fail()); + model.onDidChangeDirty(() => assert.fail()); await model.load(); assert.ok(model.isResolved()); @@ -303,7 +284,7 @@ suite('Files - TextFileEditorModel', () => { const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); - model.onDidRevert(e => eventCounter++); + model.onDidRevert(() => eventCounter++); let workingCopyEvent = false; accessor.workingCopyService.onDidChangeDirty(e => { @@ -336,7 +317,7 @@ suite('Files - TextFileEditorModel', () => { const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); - model.onDidRevert(e => eventCounter++); + model.onDidRevert(() => eventCounter++); let workingCopyEvent = false; accessor.workingCopyService.onDidChangeDirty(e => { @@ -402,7 +383,7 @@ suite('Files - TextFileEditorModel', () => { await model.revert({ soft: true }); assert.ok(!model.isDirty()); - model.onDidChangeDirty(e => eventCounter++); + model.onDidChangeDirty(() => eventCounter++); let workingCopyEvent = false; accessor.workingCopyService.onDidChangeDirty(e => { @@ -431,7 +412,7 @@ suite('Files - TextFileEditorModel', () => { } }); - const model = instantiationService.createInstance(TestTextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + const model = instantiationService.createInstance(TestReadonlyTextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); await model.load(); model.updateTextEditorModel(createTextBufferFactory('foo')); @@ -507,7 +488,7 @@ suite('Files - TextFileEditorModel', () => { let eventCounter = 0; const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); - model.onDidSave(e => { + model.onDidSave(() => { assert.equal(snapshotToString(model.createSnapshot()!), eventCounter === 1 ? 'bar' : 'foobar'); assert.ok(!model.isDirty()); eventCounter++; @@ -544,7 +525,7 @@ suite('Files - TextFileEditorModel', () => { const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); const participant = accessor.textFileService.files.addSaveParticipant({ - participate: async model => { + participate: async () => { eventCounter++; } }); @@ -563,7 +544,7 @@ suite('Files - TextFileEditorModel', () => { let eventCounter = 0; const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); - model.onDidSave(e => { + model.onDidSave(() => { assert.ok(!model.isDirty()); eventCounter++; }); @@ -595,7 +576,7 @@ suite('Files - TextFileEditorModel', () => { const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); const participant = accessor.textFileService.files.addSaveParticipant({ - participate: async model => { + participate: async () => { new Error('boom'); } }); diff --git a/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts index 89e8379ac1c..ec0904baeb6 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts @@ -7,31 +7,22 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; -import { workbenchInstantiationService, TestFileService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; -import { IFileService, FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files'; -import { IModelService } from 'vs/editor/common/services/modelService'; +import { FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files'; import { toResource } from 'vs/base/test/common/utils'; import { ModesRegistry, PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; import { ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; -class ServiceAccessor { - constructor( - @IFileService public fileService: TestFileService, - @IModelService public modelService: IModelService - ) { - } -} - suite('Files - TextFileEditorModelManager', () => { let instantiationService: IInstantiationService; - let accessor: ServiceAccessor; + let accessor: TestServiceAccessor; setup(() => { instantiationService = workbenchInstantiationService(); - accessor = instantiationService.createInstance(ServiceAccessor); + accessor = instantiationService.createInstance(TestServiceAccessor); }); test('add, remove, clear, get, getAll', function () { diff --git a/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts index 2279232737c..d90c11ca710 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts @@ -4,44 +4,21 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; -import { TestLifecycleService, TestContextService, TestFileService, TestFilesConfigurationService, TestFileDialogService, TestTextFileService, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; import { toResource } from 'vs/base/test/common/utils'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { IFileService } from 'vs/platform/files/common/files'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; -import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; -import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; - -class ServiceAccessor { - constructor( - @ILifecycleService public lifecycleService: TestLifecycleService, - @ITextFileService public textFileService: TestTextFileService, - @IWorkingCopyFileService public workingCopyFileService: IWorkingCopyFileService, - @IFilesConfigurationService public filesConfigurationService: TestFilesConfigurationService, - @IWorkspaceContextService public contextService: TestContextService, - @IModelService public modelService: ModelServiceImpl, - @IFileService public fileService: TestFileService, - @IFileDialogService public fileDialogService: TestFileDialogService - ) { - } -} suite('Files - TextFileService', () => { let instantiationService: IInstantiationService; let model: TextFileEditorModel; - let accessor: ServiceAccessor; + let accessor: TestServiceAccessor; setup(() => { instantiationService = workbenchInstantiationService(); - accessor = instantiationService.createInstance(ServiceAccessor); + accessor = instantiationService.createInstance(TestServiceAccessor); }); teardown(() => { diff --git a/src/vs/workbench/services/textfile/test/electron-browser/textFileService.io.test.ts b/src/vs/workbench/services/textfile/test/electron-browser/textFileService.io.test.ts index d50d6a3bf5e..7be9d0e1c6d 100644 --- a/src/vs/workbench/services/textfile/test/electron-browser/textFileService.io.test.ts +++ b/src/vs/workbench/services/textfile/test/electron-browser/textFileService.io.test.ts @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { ITextFileService, snapshotToString, TextFileOperationResult, TextFileOperationError } from 'vs/workbench/services/textfile/common/textfiles'; @@ -20,57 +21,23 @@ import { generateUuid } from 'vs/base/common/uuid'; import { join, basename } from 'vs/base/common/path'; import { getPathFromAmdModule } from 'vs/base/common/amd'; import { UTF16be, UTF16le, UTF8_with_bom, UTF8 } from 'vs/base/node/encoding'; -import { NativeTextFileService, EncodingOracle, IEncodingOverride } from 'vs/workbench/services/textfile/electron-browser/nativeTextFileService'; import { DefaultEndOfLine, ITextSnapshot } from 'vs/editor/common/model'; -import { TextModel } from 'vs/editor/common/model/textModel'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { isWindows } from 'vs/base/common/platform'; import { readFileSync, statSync } from 'fs'; import { detectEncodingByBOM } from 'vs/base/test/node/encoding/encoding.test'; -import { workbenchInstantiationService, TestTextFileService } from 'vs/workbench/test/electron-browser/workbenchTestServices'; - -class ServiceAccessor { - constructor( - @ITextFileService public textFileService: TestTextFileService - ) { - } -} - -class TestNativeTextFileService extends NativeTextFileService { - - private _testEncoding: TestEncodingOracle | undefined; - get encoding(): TestEncodingOracle { - if (!this._testEncoding) { - this._testEncoding = this._register(this.instantiationService.createInstance(TestEncodingOracle)); - } - - return this._testEncoding; - } -} - -class TestEncodingOracle extends EncodingOracle { - - protected get encodingOverrides(): IEncodingOverride[] { - return [ - { extension: 'utf16le', encoding: UTF16le }, - { extension: 'utf16be', encoding: UTF16be }, - { extension: 'utf8bom', encoding: UTF8_with_bom } - ]; - } - - protected set encodingOverrides(overrides: IEncodingOverride[]) { } -} +import { workbenchInstantiationService, TestNativeTextFileServiceWithEncodingOverrides } from 'vs/workbench/test/electron-browser/workbenchTestServices'; suite('Files - TextFileService i/o', () => { const parentDir = getRandomTestPath(tmpdir(), 'vsctests', 'textfileservice'); - let accessor: ServiceAccessor; const disposables = new DisposableStore(); + let service: ITextFileService; let testDir: string; setup(async () => { const instantiationService = workbenchInstantiationService(); - accessor = instantiationService.createInstance(ServiceAccessor); const logService = new NullLogService(); const fileService = new FileService(logService); @@ -82,7 +49,7 @@ suite('Files - TextFileService i/o', () => { const collection = new ServiceCollection(); collection.set(IFileService, fileService); - service = instantiationService.createChild(collection).createInstance(TestNativeTextFileService); + service = instantiationService.createChild(collection).createInstance(TestNativeTextFileServiceWithEncodingOverrides); const id = generateUuid(); testDir = join(parentDir, id); @@ -92,7 +59,7 @@ suite('Files - TextFileService i/o', () => { }); teardown(async () => { - (accessor.textFileService.files).dispose(); + (service.files).dispose(); disposables.clear(); @@ -185,7 +152,7 @@ suite('Files - TextFileService i/o', () => { test('create - UTF 8 BOM - empty content - snapshot', async () => { const resource = URI.file(join(testDir, 'small_new.utf8bom')); - await service.create(resource, TextModel.createFromString('').createSnapshot()); + await service.create(resource, createTextModel('').createSnapshot()); assert.equal(await exists(resource.fsPath), true); @@ -196,7 +163,7 @@ suite('Files - TextFileService i/o', () => { test('create - UTF 8 BOM - content provided - snapshot', async () => { const resource = URI.file(join(testDir, 'small_new.utf8bom')); - await service.create(resource, TextModel.createFromString('Hello World').createSnapshot()); + await service.create(resource, createTextModel('Hello World').createSnapshot()); assert.equal(await exists(resource.fsPath), true); @@ -209,7 +176,7 @@ suite('Files - TextFileService i/o', () => { }); test('write - use encoding (UTF 16 BE) - small content as snapshot', async () => { - await testEncoding(URI.file(join(testDir, 'small.txt')), UTF16be, TextModel.createFromString('Hello\nWorld').createSnapshot(), 'Hello\nWorld'); + await testEncoding(URI.file(join(testDir, 'small.txt')), UTF16be, createTextModel('Hello\nWorld').createSnapshot(), 'Hello\nWorld'); }); test('write - use encoding (UTF 16 BE) - large content as string', async () => { @@ -217,7 +184,7 @@ suite('Files - TextFileService i/o', () => { }); test('write - use encoding (UTF 16 BE) - large content as snapshot', async () => { - await testEncoding(URI.file(join(testDir, 'lorem.txt')), UTF16be, TextModel.createFromString('Hello\nWorld').createSnapshot(), 'Hello\nWorld'); + await testEncoding(URI.file(join(testDir, 'lorem.txt')), UTF16be, createTextModel('Hello\nWorld').createSnapshot(), 'Hello\nWorld'); }); async function testEncoding(resource: URI, encoding: string, content: string | ITextSnapshot, expectedContent: string) { @@ -265,7 +232,7 @@ suite('Files - TextFileService i/o', () => { resolved = await service.readStream(resource, { encoding }); assert.equal(snapshotToString(resolved.value.create(DefaultEndOfLine.CRLF).createSnapshot(false)), content); - await service.write(resource, TextModel.createFromString(content).createSnapshot(), { encoding }); + await service.write(resource, createTextModel(content).createSnapshot(), { encoding }); resolved = await service.readStream(resource, { encoding }); assert.equal(snapshotToString(resolved.value.create(DefaultEndOfLine.CRLF).createSnapshot(false)), content); @@ -287,7 +254,7 @@ suite('Files - TextFileService i/o', () => { const content = (await readFile(resource.fsPath)).toString(); - await service.write(resource, TextModel.createFromString(content).createSnapshot()); + await service.write(resource, createTextModel(content).createSnapshot()); const resolved = await service.readStream(resource); assert.equal(resolved.value.getFirstLineText(999999), content); @@ -308,7 +275,7 @@ suite('Files - TextFileService i/o', () => { const resolved = await service.readStream(resource); assert.equal(resolved.encoding, UTF16le); - await testEncoding(URI.file(join(testDir, 'some_utf16le.css')), UTF16le, TextModel.createFromString('Hello\nWorld').createSnapshot(), 'Hello\nWorld'); + await testEncoding(URI.file(join(testDir, 'some_utf16le.css')), UTF16le, createTextModel('Hello\nWorld').createSnapshot(), 'Hello\nWorld'); }); test('write - UTF8 variations - content as string', async () => { @@ -345,7 +312,7 @@ suite('Files - TextFileService i/o', () => { let detectedEncoding = await detectEncodingByBOM(resource.fsPath); assert.equal(detectedEncoding, null); - const model = TextModel.createFromString((await readFile(resource.fsPath)).toString() + 'updates'); + const model = createTextModel((await readFile(resource.fsPath)).toString() + 'updates'); await service.write(resource, model.createSnapshot(), { encoding: UTF8_with_bom }); detectedEncoding = await detectEncodingByBOM(resource.fsPath); @@ -390,7 +357,7 @@ suite('Files - TextFileService i/o', () => { test('write - ensure BOM in empty file - content as snapshot', async () => { const resource = URI.file(join(testDir, 'small.txt')); - await service.write(resource, TextModel.createFromString('').createSnapshot(), { encoding: UTF8_with_bom }); + await service.write(resource, createTextModel('').createSnapshot(), { encoding: UTF8_with_bom }); let detectedEncoding = await detectEncodingByBOM(resource.fsPath); assert.equal(detectedEncoding, UTF8_with_bom); @@ -413,7 +380,7 @@ suite('Files - TextFileService i/o', () => { assert.equal(result.name, basename(resource.fsPath)); assert.equal(result.size, statSync(resource.fsPath).size); - assert.equal(snapshotToString(result.value.create(DefaultEndOfLine.LF).createSnapshot(false)), snapshotToString(TextModel.createFromString(readFileSync(resource.fsPath).toString()).createSnapshot(false))); + assert.equal(snapshotToString(result.value.create(DefaultEndOfLine.LF).createSnapshot(false)), snapshotToString(createTextModel(readFileSync(resource.fsPath).toString()).createSnapshot(false))); } test('read - small text', async () => { diff --git a/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts b/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts index b322438b8d4..d8ca80f38f4 100644 --- a/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts +++ b/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts @@ -9,39 +9,24 @@ import { URI } from 'vs/base/common/uri'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; import { toResource } from 'vs/base/test/common/utils'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { IModeService } from 'vs/editor/common/services/modeService'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; -import { ITextFileService, snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; -import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; +import { snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { Event } from 'vs/base/common/event'; import { timeout } from 'vs/base/common/async'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; -class ServiceAccessor { - constructor( - @ITextModelService public textModelResolverService: ITextModelService, - @IModelService public modelService: IModelService, - @IModeService public modeService: IModeService, - @ITextFileService public textFileService: ITextFileService, - @IUntitledTextEditorService public untitledTextEditorService: IUntitledTextEditorService - ) { - } -} - suite('Workbench - TextModelResolverService', () => { let instantiationService: IInstantiationService; - let accessor: ServiceAccessor; + let accessor: TestServiceAccessor; let model: TextFileEditorModel; setup(() => { instantiationService = workbenchInstantiationService(); - accessor = instantiationService.createInstance(ServiceAccessor); + accessor = instantiationService.createInstance(TestServiceAccessor); }); teardown(() => { diff --git a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts index 05e918a797d..e42c09b2e33 100644 --- a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts +++ b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts @@ -92,7 +92,7 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt // Take name from first line if present and only if // we have no associated file path. In that case we // prefer the file name as title. - if (!this.hasAssociatedFilePath && this.cachedModelFirstLineWords) { + if (this.configuredLabelFormat === 'content' && !this.hasAssociatedFilePath && this.cachedModelFirstLineWords) { return this.cachedModelFirstLineWords; } @@ -100,11 +100,13 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt return this.labelService.getUriBasenameLabel(this.resource); } - private dirty = false; + private dirty = this.hasAssociatedFilePath || !!this.initialValue; private ignoreDirtyOnModelContentChange = false; private versionId = 0; + private configuredEncoding: string | undefined; + private configuredLabelFormat: 'content' | 'name' = 'content'; constructor( public readonly resource: URI, @@ -130,25 +132,39 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt this.setMode(preferredMode); } + // Fetch config + this.onConfigurationChange(false); + this.registerListeners(); } private registerListeners(): void { // Config Changes - this._register(this.textResourceConfigurationService.onDidChangeConfiguration(e => this.onConfigurationChange())); + this._register(this.textResourceConfigurationService.onDidChangeConfiguration(e => this.onConfigurationChange(true))); } - private onConfigurationChange(): void { - const configuredEncoding = this.textResourceConfigurationService.getValue(this.resource, 'files.encoding'); + private onConfigurationChange(fromEvent: boolean): void { + // Encoding + const configuredEncoding = this.textResourceConfigurationService.getValue(this.resource, 'files.encoding'); if (this.configuredEncoding !== configuredEncoding) { this.configuredEncoding = configuredEncoding; - if (!this.preferredEncoding) { + if (fromEvent && !this.preferredEncoding) { this._onDidChangeEncoding.fire(); // do not fire event if we have a preferred encoding set } } + + // Label Format + const configuredLabelFormat = this.textResourceConfigurationService.getValue(this.resource, 'workbench.editor.untitled.labelFormat'); + if (this.configuredLabelFormat !== configuredLabelFormat && (configuredLabelFormat === 'content' || configuredLabelFormat === 'name')) { + this.configuredLabelFormat = configuredLabelFormat; + + if (fromEvent) { + this._onDidChangeName.fire(); + } + } } getVersionId(): number { @@ -281,13 +297,10 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt this.updateTextEditorModel(undefined, this.preferredMode); } - // Figure out encoding now that model is present - this.configuredEncoding = this.textResourceConfigurationService.getValue(this.resource, 'files.encoding'); - // Listen to text model events const textEditorModel = assertIsDefined(this.textEditorModel); this._register(textEditorModel.onDidChangeContent(e => this.onModelContentChanged(textEditorModel, e))); - this._register(textEditorModel.onDidChangeLanguage(() => this.onConfigurationChange())); // mode change can have impact on config + this._register(textEditorModel.onDidChangeLanguage(() => this.onConfigurationChange(true))); // mode change can have impact on config // Only adjust name and dirty state etc. if we // actually created the untitled model diff --git a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts index 7234536f6e9..9b2b8015372 100644 --- a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts +++ b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts @@ -8,37 +8,21 @@ import * as assert from 'assert'; import { join } from 'vs/base/common/path'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IUntitledTextEditorService, UntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { workbenchInstantiationService, TestEditorService } from 'vs/workbench/test/browser/workbenchTestServices'; -import { IModeService } from 'vs/editor/common/services/modeService'; -import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl'; +import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; import { snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; import { ModesRegistry, PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; -import { IWorkingCopyService, IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; import { Range } from 'vs/editor/common/core/range'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; -class ServiceAccessor { - constructor( - @IUntitledTextEditorService public readonly untitledTextEditorService: IUntitledTextEditorService, - @IEditorService public readonly editorService: TestEditorService, - @IWorkingCopyService public readonly workingCopyService: IWorkingCopyService, - @IModeService public readonly modeService: ModeServiceImpl, - @IConfigurationService public readonly testConfigurationService: TestConfigurationService - ) { } -} - suite('Untitled text editors', () => { let instantiationService: IInstantiationService; - let accessor: ServiceAccessor; + let accessor: TestServiceAccessor; setup(() => { instantiationService = workbenchInstantiationService(); - accessor = instantiationService.createInstance(ServiceAccessor); + accessor = instantiationService.createInstance(TestServiceAccessor); }); teardown(() => { @@ -135,9 +119,13 @@ suite('Untitled text editors', () => { test('associated resource is dirty', async () => { const service = accessor.untitledTextEditorService; const file = URI.file(join('C:\\', '/foo/file.txt')); - const untitled = await service.resolve({ associatedResource: file }); - assert.ok(untitled.hasAssociatedFilePath); + const untitled = instantiationService.createInstance(UntitledTextEditorInput, service.create({ associatedResource: file })); + assert.ok(untitled.isDirty()); + + const model = await untitled.resolve(); + + assert.ok(model.hasAssociatedFilePath); assert.equal(untitled.isDirty(), true); untitled.dispose(); @@ -212,20 +200,14 @@ suite('Untitled text editors', () => { const workingCopyService = accessor.workingCopyService; const untitled = instantiationService.createInstance(UntitledTextEditorInput, service.create({ initialValue: 'Hello World' })); - - let onDidChangeDirty: IWorkingCopy | undefined = undefined; - const listener = workingCopyService.onDidChangeDirty(copy => { - onDidChangeDirty = copy; - }); + assert.ok(untitled.isDirty()); // dirty const model = await untitled.resolve(); assert.ok(model.isDirty()); assert.equal(workingCopyService.dirtyCount, 1); - assert.equal(onDidChangeDirty, model); untitled.dispose(); - listener.dispose(); model.dispose(); }); diff --git a/src/vs/workbench/services/userDataSync/common/userDataSyncUtil.ts b/src/vs/workbench/services/userDataSync/common/userDataSyncUtil.ts index c168d05c76a..0fbeb2a56c8 100644 --- a/src/vs/workbench/services/userDataSync/common/userDataSyncUtil.ts +++ b/src/vs/workbench/services/userDataSync/common/userDataSyncUtil.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncUtilService, getDefaultIgnoredSettings } from 'vs/platform/userDataSync/common/userDataSync'; import { IStringDictionary } from 'vs/base/common/collections'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; @@ -23,7 +23,11 @@ class UserDataSyncUtilService implements IUserDataSyncUtilService { @ITextResourceConfigurationService private readonly textResourceConfigurationService: ITextResourceConfigurationService, ) { } - public async resolveUserBindings(userBindings: string[]): Promise> { + async resolveDefaultIgnoredSettings(): Promise { + return getDefaultIgnoredSettings(); + } + + async resolveUserBindings(userBindings: string[]): Promise> { const keys: IStringDictionary = {}; for (const userbinding of userBindings) { keys[userbinding] = this.keybindingsService.resolveUserBinding(userbinding).map(part => part.getUserSettingsLabel()).join(' '); diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService.ts index 4dc5de8bc46..88ee0dcb78f 100644 --- a/src/vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService.ts +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService.ts @@ -24,8 +24,8 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto this.channel = sharedProcessService.getChannel('userDataAutoSync'); } - triggerAutoSync(): Promise { - return this.channel.call('triggerAutoSync'); + triggerAutoSync(sources: string[]): Promise { + return this.channel.call('triggerAutoSync', [sources]); } } diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts index 46537301337..b3ec8ffeb95 100644 --- a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts @@ -22,7 +22,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ private _onDidChangeStatus: Emitter = this._register(new Emitter()); readonly onDidChangeStatus: Event = this._onDidChangeStatus.event; - get onDidChangeLocal(): Event { return this.channel.listen('onDidChangeLocal'); } + get onDidChangeLocal(): Event { return this.channel.listen('onDidChangeLocal'); } private _conflictsSources: SyncSource[] = []; get conflictsSources(): SyncSource[] { return this._conflictsSources; } @@ -34,6 +34,9 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ private _onDidChangeLastSyncTime: Emitter = this._register(new Emitter()); readonly onDidChangeLastSyncTime: Event = this._onDidChangeLastSyncTime.event; + private _onSyncErrors: Emitter<[SyncSource, UserDataSyncError][]> = this._register(new Emitter<[SyncSource, UserDataSyncError][]>()); + readonly onSyncErrors: Event<[SyncSource, UserDataSyncError][]> = this._onSyncErrors.event; + constructor( @ISharedProcessService sharedProcessService: ISharedProcessService ) { @@ -58,6 +61,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ this._register(this.channel.listen('onDidChangeLastSyncTime')(lastSyncTime => this.updateLastSyncTime(lastSyncTime))); }); this._register(this.channel.listen('onDidChangeConflicts')(conflicts => this.updateConflicts(conflicts))); + this._register(this.channel.listen<[SyncSource, Error][]>('onSyncErrors')(errors => this._onSyncErrors.fire(errors.map(([source, error]) => ([source, UserDataSyncError.toUserDataSyncError(error)]))))); } pull(): Promise { diff --git a/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts b/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts index ffcf97d9edc..9d05c8dd213 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts @@ -8,31 +8,19 @@ import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textF import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { toResource } from 'vs/base/test/common/utils'; -import { workbenchInstantiationService, TestTextFileService } from 'vs/workbench/test/browser/workbenchTestServices'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; -import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; import { URI } from 'vs/base/common/uri'; import { FileOperation } from 'vs/platform/files/common/files'; -class ServiceAccessor { - constructor( - @ITextFileService public textFileService: TestTextFileService, - @IWorkingCopyFileService public workingCopyFileService: IWorkingCopyFileService, - @IWorkingCopyService public workingCopyService: IWorkingCopyService - ) { - } -} - suite('WorkingCopyFileService', () => { let instantiationService: IInstantiationService; let model: TextFileEditorModel; - let accessor: ServiceAccessor; + let accessor: TestServiceAccessor; setup(() => { instantiationService = workbenchInstantiationService(); - accessor = instantiationService.createInstance(ServiceAccessor); + accessor = instantiationService.createInstance(TestServiceAccessor); }); teardown(() => { diff --git a/src/vs/workbench/test/browser/api/extHostApiCommands.test.ts b/src/vs/workbench/test/browser/api/extHostApiCommands.test.ts index 506745c103a..ccdf0740f5b 100644 --- a/src/vs/workbench/test/browser/api/extHostApiCommands.test.ts +++ b/src/vs/workbench/test/browser/api/extHostApiCommands.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { setUnexpectedErrorHandler, errorHandler } from 'vs/base/common/errors'; import { URI } from 'vs/base/common/uri'; import * as types from 'vs/workbench/api/common/extHostTypes'; -import { TextModel } from 'vs/editor/common/model/textModel'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { TestRPCProtocol } from './testRPCProtocol'; import { MarkerService } from 'vs/platform/markers/common/markerService'; import { IMarkerService } from 'vs/platform/markers/common/markers'; @@ -49,7 +49,7 @@ import 'vs/editor/contrib/smartSelect/smartSelect'; import 'vs/editor/contrib/suggest/suggest'; const defaultSelector = { scheme: 'far' }; -const model: ITextModel = TextModel.createFromString( +const model: ITextModel = createTextModel( [ 'This is the first line', 'This is the second line', diff --git a/src/vs/workbench/test/browser/api/extHostLanguageFeatures.test.ts b/src/vs/workbench/test/browser/api/extHostLanguageFeatures.test.ts index 07285eb64e6..148148e352e 100644 --- a/src/vs/workbench/test/browser/api/extHostLanguageFeatures.test.ts +++ b/src/vs/workbench/test/browser/api/extHostLanguageFeatures.test.ts @@ -8,7 +8,7 @@ import { TestInstantiationService } from 'vs/platform/instantiation/test/common/ import { setUnexpectedErrorHandler, errorHandler } from 'vs/base/common/errors'; import { URI } from 'vs/base/common/uri'; import * as types from 'vs/workbench/api/common/extHostTypes'; -import { TextModel } from 'vs/editor/common/model/textModel'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { Position as EditorPosition, Position } from 'vs/editor/common/core/position'; import { Range as EditorRange } from 'vs/editor/common/core/range'; import { TestRPCProtocol } from './testRPCProtocol'; @@ -48,9 +48,10 @@ import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerServ import { dispose } from 'vs/base/common/lifecycle'; import { withNullAsUndefined } from 'vs/base/common/types'; import { NullApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; +import { Progress } from 'vs/platform/progress/common/progress'; const defaultSelector = { scheme: 'far' }; -const model: ITextModel = TextModel.createFromString( +const model: ITextModel = createTextModel( [ 'This is the first line', 'This is the second line', @@ -590,7 +591,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const { validActions: actions } = await getCodeActions(model, model.getFullModelRange(), { type: modes.CodeActionTriggerType.Manual }, CancellationToken.None); + const { validActions: actions } = await getCodeActions(model, model.getFullModelRange(), { type: modes.CodeActionTriggerType.Manual }, Progress.None, CancellationToken.None); assert.equal(actions.length, 2); const [first, second] = actions; assert.equal(first.title, 'Testing1'); @@ -614,7 +615,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const { validActions: actions } = await getCodeActions(model, model.getFullModelRange(), { type: modes.CodeActionTriggerType.Manual }, CancellationToken.None); + const { validActions: actions } = await getCodeActions(model, model.getFullModelRange(), { type: modes.CodeActionTriggerType.Manual }, Progress.None, CancellationToken.None); assert.equal(actions.length, 1); const [first] = actions; assert.equal(first.title, 'Testing1'); @@ -637,7 +638,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const { validActions: actions } = await getCodeActions(model, model.getFullModelRange(), { type: modes.CodeActionTriggerType.Manual }, CancellationToken.None); + const { validActions: actions } = await getCodeActions(model, model.getFullModelRange(), { type: modes.CodeActionTriggerType.Manual }, Progress.None, CancellationToken.None); assert.equal(actions.length, 1); }); @@ -655,7 +656,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const { validActions: actions } = await getCodeActions(model, model.getFullModelRange(), { type: modes.CodeActionTriggerType.Manual }, CancellationToken.None); + const { validActions: actions } = await getCodeActions(model, model.getFullModelRange(), { type: modes.CodeActionTriggerType.Manual }, Progress.None, CancellationToken.None); assert.equal(actions.length, 1); }); diff --git a/src/vs/workbench/test/browser/api/extHostWebview.test.ts b/src/vs/workbench/test/browser/api/extHostWebview.test.ts index ed88cc6018e..c3e830dd4b9 100644 --- a/src/vs/workbench/test/browser/api/extHostWebview.test.ts +++ b/src/vs/workbench/test/browser/api/extHostWebview.test.ts @@ -13,19 +13,33 @@ import { ExtHostWebviews } from 'vs/workbench/api/common/extHostWebview'; import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; import { mock } from 'vs/workbench/test/browser/api/mock'; import { SingleProxyRPCProtocol } from './testRPCProtocol'; +import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; +import { IExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; import { NullApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; suite('ExtHostWebview', () => { + let rpcProtocol: (IExtHostRpcService & IExtHostContext) | undefined; + let extHostDocuments: ExtHostDocuments | undefined; + + setup(() => { + const shape = createNoopMainThreadWebviews(); + rpcProtocol = SingleProxyRPCProtocol(shape); + + const extHostDocumentsAndEditors = new ExtHostDocumentsAndEditors(rpcProtocol, new NullLogService()); + extHostDocuments = new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors); + }); + test('Cannot register multiple serializers for the same view type', async () => { const viewType = 'view.type'; - const shape = createNoopMainThreadWebviews(); - const extHostWebviews = new ExtHostWebviews(SingleProxyRPCProtocol(shape), { + const extHostWebviews = new ExtHostWebviews(rpcProtocol!, { webviewCspSource: '', webviewResourceRoot: '', isExtensionDevelopmentDebug: false, - }, undefined, new NullLogService(), NullApiDeprecationService); + }, undefined, new NullLogService(), NullApiDeprecationService, extHostDocuments!); let lastInvokedDeserializer: vscode.WebviewPanelSerializer | undefined = undefined; @@ -58,12 +72,11 @@ suite('ExtHostWebview', () => { }); test('asWebviewUri for desktop vscode-resource scheme', () => { - const shape = createNoopMainThreadWebviews(); - const extHostWebviews = new ExtHostWebviews(SingleProxyRPCProtocol(shape), { + const extHostWebviews = new ExtHostWebviews(rpcProtocol!, { webviewCspSource: '', webviewResourceRoot: 'vscode-resource://{{resource}}', isExtensionDevelopmentDebug: false, - }, undefined, new NullLogService(), NullApiDeprecationService); + }, undefined, new NullLogService(), NullApiDeprecationService, extHostDocuments!); const webview = extHostWebviews.createWebviewPanel({} as any, 'type', 'title', 1, {}); assert.strictEqual( @@ -98,13 +111,11 @@ suite('ExtHostWebview', () => { }); test('asWebviewUri for web endpoint', () => { - const shape = createNoopMainThreadWebviews(); - - const extHostWebviews = new ExtHostWebviews(SingleProxyRPCProtocol(shape), { + const extHostWebviews = new ExtHostWebviews(rpcProtocol!, { webviewCspSource: '', webviewResourceRoot: `https://{{uuid}}.webview.contoso.com/commit/{{resource}}`, isExtensionDevelopmentDebug: false, - }, undefined, new NullLogService(), NullApiDeprecationService); + }, undefined, new NullLogService(), NullApiDeprecationService, extHostDocuments!); const webview = extHostWebviews.createWebviewPanel({} as any, 'type', 'title', 1, {}); function stripEndpointUuid(input: string) { diff --git a/src/vs/workbench/test/browser/api/mainThreadDocumentContentProviders.test.ts b/src/vs/workbench/test/browser/api/mainThreadDocumentContentProviders.test.ts index dfef5fee0f5..234f1163ae4 100644 --- a/src/vs/workbench/test/browser/api/mainThreadDocumentContentProviders.test.ts +++ b/src/vs/workbench/test/browser/api/mainThreadDocumentContentProviders.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { MainThreadDocumentContentProviders } from 'vs/workbench/api/browser/mainThreadDocumentContentProviders'; -import { TextModel } from 'vs/editor/common/model/textModel'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { mock } from 'vs/workbench/test/browser/api/mock'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; @@ -18,7 +18,7 @@ suite('MainThreadDocumentContentProviders', function () { test('events are processed properly', function () { let uri = URI.parse('test:uri'); - let model = TextModel.createFromString('1', undefined, undefined, uri); + let model = createTextModel('1', undefined, undefined, uri); let providers = new MainThreadDocumentContentProviders(new TestRPCProtocol(), null!, null!, new class extends mock() { diff --git a/src/vs/workbench/test/browser/api/mainThreadDocuments.test.ts b/src/vs/workbench/test/browser/api/mainThreadDocuments.test.ts index 8ec19753623..a14eca8b02a 100644 --- a/src/vs/workbench/test/browser/api/mainThreadDocuments.test.ts +++ b/src/vs/workbench/test/browser/api/mainThreadDocuments.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { BoundModelReferenceCollection } from 'vs/workbench/api/browser/mainThreadDocuments'; -import { TextModel } from 'vs/editor/common/model/textModel'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { timeout } from 'vs/base/common/async'; suite('BoundModelReferenceCollection', () => { @@ -21,7 +21,7 @@ suite('BoundModelReferenceCollection', () => { let didDispose = false; col.add({ - object: { textEditorModel: TextModel.createFromString('farboo') }, + object: { textEditorModel: createTextModel('farboo') }, dispose() { didDispose = true; } @@ -36,20 +36,20 @@ suite('BoundModelReferenceCollection', () => { let disposed: number[] = []; col.add({ - object: { textEditorModel: TextModel.createFromString('farboo') }, + object: { textEditorModel: createTextModel('farboo') }, dispose() { disposed.push(0); } }); col.add({ - object: { textEditorModel: TextModel.createFromString('boofar') }, + object: { textEditorModel: createTextModel('boofar') }, dispose() { disposed.push(1); } }); col.add({ - object: { textEditorModel: TextModel.createFromString(new Array(71).join('x')) }, + object: { textEditorModel: createTextModel(new Array(71).join('x')) }, dispose() { disposed.push(2); } diff --git a/src/vs/workbench/test/browser/api/mainThreadDocumentsAndEditors.test.ts b/src/vs/workbench/test/browser/api/mainThreadDocumentsAndEditors.test.ts index 24f0c27176f..4101a887f69 100644 --- a/src/vs/workbench/test/browser/api/mainThreadDocumentsAndEditors.test.ts +++ b/src/vs/workbench/test/browser/api/mainThreadDocumentsAndEditors.test.ts @@ -23,6 +23,8 @@ import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { NullLogService } from 'vs/platform/log/common/log'; import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; +import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; suite('MainThreadDocumentsAndEditors', () => { @@ -45,7 +47,10 @@ suite('MainThreadDocumentsAndEditors', () => { deltas.length = 0; const configService = new TestConfigurationService(); configService.setUserConfiguration('editor', { 'detectIndentation': false }); - modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), new UndoRedoService()); + const dialogService = new TestDialogService(); + const notificationService = new TestNotificationService(); + const undoRedoService = new UndoRedoService(dialogService, notificationService); + modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), undoRedoService); codeEditorService = new TestCodeEditorService(); textFileService = new class extends mock() { isDirty() { return false; } diff --git a/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts b/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts index ef650b280c6..0d96189e4c9 100644 --- a/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts +++ b/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts @@ -42,6 +42,11 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { ILabelService } from 'vs/platform/label/common/label'; import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; +import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; +import { INotificationService } from 'vs/platform/notification/common/notification'; suite('MainThreadEditors', () => { @@ -64,7 +69,10 @@ suite('MainThreadEditors', () => { const configService = new TestConfigurationService(); - modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), new UndoRedoService()); + const dialogService = new TestDialogService(); + const notificationService = new TestNotificationService(); + const undoRedoService = new UndoRedoService(dialogService, notificationService); + modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService(), undoRedoService); const services = new ServiceCollection(); @@ -74,6 +82,9 @@ suite('MainThreadEditors', () => { services.set(IWorkspaceContextService, new TestContextService()); services.set(IWorkbenchEnvironmentService, TestEnvironmentService); services.set(IConfigurationService, configService); + services.set(IDialogService, dialogService); + services.set(INotificationService, notificationService); + services.set(IUndoRedoService, undoRedoService); services.set(IModelService, modelService); services.set(ICodeEditorService, new TestCodeEditorService()); services.set(IFileService, new TestFileService()); diff --git a/src/vs/workbench/test/browser/parts/editor/editor.test.ts b/src/vs/workbench/test/browser/parts/editor/editor.test.ts index d86598da7ff..96f97d1ad98 100644 --- a/src/vs/workbench/test/browser/parts/editor/editor.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editor.test.ts @@ -4,43 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { EditorInput, toResource, SideBySideEditor } from 'vs/workbench/common/editor'; +import { toResource, SideBySideEditor } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; -import { IEditorModel } from 'vs/platform/editor/common/editor'; import { URI } from 'vs/base/common/uri'; -import { IUntitledTextEditorService, UntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { workbenchInstantiationService, TestServiceAccessor, TestEditorInput } from 'vs/workbench/test/browser/workbenchTestServices'; import { Schemas } from 'vs/base/common/network'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; -class ServiceAccessor { - constructor(@IUntitledTextEditorService public untitledTextEditorService: UntitledTextEditorService) { } -} - -class FileEditorInput extends EditorInput { - - constructor(public resource: URI) { - super(); - } - - getTypeId(): string { - return 'editorResourceFileTest'; - } - - resolve(): Promise { - return Promise.resolve(null); - } -} - suite('Workbench editor', () => { let instantiationService: IInstantiationService; - let accessor: ServiceAccessor; + let accessor: TestServiceAccessor; setup(() => { instantiationService = workbenchInstantiationService(); - accessor = instantiationService.createInstance(ServiceAccessor); + accessor = instantiationService.createInstance(TestServiceAccessor); }); teardown(() => { @@ -60,7 +39,7 @@ suite('Workbench editor', () => { assert.equal(toResource(untitled, { filterByScheme: [Schemas.file, Schemas.untitled] })!.toString(), untitled.resource.toString()); assert.ok(!toResource(untitled, { filterByScheme: Schemas.file })); - const file = new FileEditorInput(URI.file('/some/path.txt')); + const file = new TestEditorInput(URI.file('/some/path.txt'), 'editorResourceFileTest'); assert.equal(toResource(file)!.toString(), file.resource.toString()); assert.equal(toResource(file, { supportSideBySide: SideBySideEditor.MASTER })!.toString(), file.resource.toString()); diff --git a/src/vs/workbench/test/browser/parts/editor/editorDiffModel.test.ts b/src/vs/workbench/test/browser/parts/editor/editorDiffModel.test.ts index 369db0b1abb..37bfc6dc478 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorDiffModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorDiffModel.test.ts @@ -6,31 +6,20 @@ import * as assert from 'assert'; import { TextDiffEditorModel } from 'vs/workbench/common/editor/textDiffEditorModel'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { IModeService } from 'vs/editor/common/services/modeService'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { URI } from 'vs/base/common/uri'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; import { ITextModel } from 'vs/editor/common/model'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -class ServiceAccessor { - constructor( - @ITextModelService public textModelResolverService: ITextModelService, - @IModelService public modelService: IModelService, - @IModeService public modeService: IModeService, - ) { - } -} - suite('Workbench editor model', () => { + let instantiationService: IInstantiationService; - let accessor: ServiceAccessor; + let accessor: TestServiceAccessor; setup(() => { instantiationService = workbenchInstantiationService(); - accessor = instantiationService.createInstance(ServiceAccessor); + accessor = instantiationService.createInstance(TestServiceAccessor); }); test('TextDiffEditorModel', async () => { diff --git a/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts b/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts index 849a327b502..d0c2abb5eea 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts @@ -20,6 +20,10 @@ import { ITextResourcePropertiesService } from 'vs/editor/common/services/textRe import { TestTextResourcePropertiesService } from 'vs/workbench/test/browser/workbenchTestServices'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; +import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; +import { INotificationService } from 'vs/platform/notification/common/notification'; class MyEditorModel extends EditorModel { } class MyTextEditorModel extends BaseTextEditorModel { @@ -72,9 +76,14 @@ suite('Workbench editor model', () => { }); function stubModelService(instantiationService: TestInstantiationService): IModelService { + const dialogService = new TestDialogService(); + const notificationService = new TestNotificationService(); + const undoRedoService = new UndoRedoService(dialogService, notificationService); instantiationService.stub(IConfigurationService, new TestConfigurationService()); instantiationService.stub(ITextResourcePropertiesService, new TestTextResourcePropertiesService(instantiationService.get(IConfigurationService))); - instantiationService.stub(IUndoRedoService, new UndoRedoService()); + instantiationService.stub(IDialogService, dialogService); + instantiationService.stub(INotificationService, notificationService); + instantiationService.stub(IUndoRedoService, undoRedoService); return instantiationService.createInstance(ModelServiceImpl); } }); diff --git a/src/vs/workbench/test/browser/parts/editor/rangeDecorations.test.ts b/src/vs/workbench/test/browser/parts/editor/rangeDecorations.test.ts index ef2555ad3f7..06f12dbcaea 100644 --- a/src/vs/workbench/test/browser/parts/editor/rangeDecorations.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/rangeDecorations.test.ts @@ -21,6 +21,7 @@ import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; import { CoreNavigationCommands } from 'vs/editor/browser/controller/coreCommands'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; suite('Editor - Range decorations', () => { @@ -136,7 +137,7 @@ suite('Editor - Range decorations', () => { } function aModel(resource: URI, content: string = text): TextModel { - let model = TextModel.createFromString(content, TextModel.DEFAULT_CREATION_OPTIONS, null, resource); + let model = createTextModel(content, TextModel.DEFAULT_CREATION_OPTIONS, null, resource); modelsToDispose.push(model); return model; } diff --git a/src/vs/workbench/test/browser/parts/editor/resourceEditorInput.test.ts b/src/vs/workbench/test/browser/parts/editor/resourceEditorInput.test.ts index 2eaede4adbc..7634e777a07 100644 --- a/src/vs/workbench/test/browser/parts/editor/resourceEditorInput.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/resourceEditorInput.test.ts @@ -8,26 +8,18 @@ import { URI } from 'vs/base/common/uri'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { IModeService } from 'vs/editor/common/services/modeService'; +import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; import { snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; import { ModesRegistry, PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; -class ServiceAccessor { - constructor( - @IModelService public modelService: IModelService, - @IModeService public modeService: IModeService - ) { } -} - suite('Resource text editors', () => { + let instantiationService: IInstantiationService; - let accessor: ServiceAccessor; + let accessor: TestServiceAccessor; setup(() => { instantiationService = workbenchInstantiationService(); - accessor = instantiationService.createInstance(ServiceAccessor); + accessor = instantiationService.createInstance(TestServiceAccessor); }); test('basics', async () => { diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 9e00ead1308..7a7f9f4ee7e 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -10,16 +10,15 @@ import * as resources from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { IEditorInputWithOptions, CloseDirection, IEditorIdentifier, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditorInput, IEditor, IEditorCloseEvent, IEditorPartOptions, IRevertOptions, GroupIdentifier, EditorInput, EditorOptions, EditorsOrder } from 'vs/workbench/common/editor'; +import { IEditorInputWithOptions, CloseDirection, IEditorIdentifier, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditorInput, IEditor, IEditorCloseEvent, IEditorPartOptions, IRevertOptions, GroupIdentifier, EditorInput, EditorOptions, EditorsOrder, IFileEditorInput, IEditorInputFactoryRegistry, IEditorInputFactory, Extensions as EditorExtensions, ISaveOptions, IMoveResult } from 'vs/workbench/common/editor'; import { IEditorOpeningEvent, EditorServiceImpl, IEditorGroupView, IEditorGroupsAccessor } from 'vs/workbench/browser/parts/editor/editor'; import { Event, Emitter } from 'vs/base/common/event'; -import Severity from 'vs/base/common/severity'; import { IBackupFileService, IResolvedBackup } from 'vs/workbench/services/backup/common/backup'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchLayoutService, Parts, Position as PartPosition } from 'vs/workbench/services/layout/browser/layoutService'; import { TextModelResolverService } from 'vs/workbench/services/textmodelResolver/common/textModelResolverService'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { IEditorOptions, IResourceInput } from 'vs/platform/editor/common/editor'; +import { IEditorOptions, IResourceInput, IEditorModel } from 'vs/platform/editor/common/editor'; import { IUntitledTextEditorService, UntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ILifecycleService, BeforeShutdownEvent, ShutdownReason, StartupKind, LifecyclePhase, WillShutdownEvent } from 'vs/platform/lifecycle/common/lifecycle'; @@ -45,17 +44,18 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { MockContextKeyService, MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { ITextBufferFactory, DefaultEndOfLine, EndOfLinePreference, IModelDecorationOptions, ITextModel, ITextSnapshot } from 'vs/editor/common/model'; import { Range } from 'vs/editor/common/core/range'; -import { IConfirmation, IConfirmationResult, IDialogService, IDialogOptions, IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, IFileDialogService, IShowResult, ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; +import { IDialogService, IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, IFileDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { IExtensionService, NullExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IDecorationsService, IResourceDecorationChangeEvent, IDecoration, IDecorationData, IDecorationsProvider } from 'vs/workbench/services/decorations/browser/decorations'; -import { IDisposable, toDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { IDisposable, toDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IEditorGroupsService, IEditorGroup, GroupsOrder, GroupsArrangement, GroupDirection, IAddGroupOptions, IMergeGroupOptions, IMoveEditorOptions, ICopyEditorOptions, IEditorReplacement, IGroupChangeEvent, IFindGroupScope, EditorGroupLayout, ICloseEditorOptions } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService, IOpenEditorOverrideHandler, IVisibleEditor, ISaveEditorsOptions, IRevertAllEditorsOptions, IResourceEditor } from 'vs/workbench/services/editor/common/editorService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; +import { IEditorRegistry, EditorDescriptor, Extensions } from 'vs/workbench/browser/editor'; import { IDecorationRenderOptions } from 'vs/editor/common/editorCommon'; import { EditorGroup } from 'vs/workbench/common/editor/editorGroup'; import { Dimension } from 'vs/base/browser/dom'; @@ -89,10 +89,16 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService'; import { Direction } from 'vs/base/browser/ui/grid/grid'; -import { IProgressService, IProgressOptions, IProgressWindowOptions, IProgressNotificationOptions, IProgressCompositeOptions, IProgress, IProgressStep, emptyProgress } from 'vs/platform/progress/common/progress'; +import { IProgressService, IProgressOptions, IProgressWindowOptions, IProgressNotificationOptions, IProgressCompositeOptions, IProgress, IProgressStep, Progress } from 'vs/platform/progress/common/progress'; import { IWorkingCopyFileService, WorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; +import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; export import TestTextResourcePropertiesService = CommonWorkbenchTestServices.TestTextResourcePropertiesService; export import TestContextService = CommonWorkbenchTestServices.TestContextService; @@ -107,6 +113,78 @@ export interface ITestInstantiationService extends IInstantiationService { stub(service: ServiceIdentifier, ctor: any): T; } +export function workbenchInstantiationService(overrides?: { textFileService?: (instantiationService: IInstantiationService) => ITextFileService }): ITestInstantiationService { + const instantiationService = new TestInstantiationService(new ServiceCollection([ILifecycleService, new TestLifecycleService()])); + + instantiationService.stub(IWorkingCopyService, new TestWorkingCopyService()); + instantiationService.stub(IEnvironmentService, TestEnvironmentService); + const contextKeyService = instantiationService.createInstance(MockContextKeyService); + instantiationService.stub(IContextKeyService, contextKeyService); + instantiationService.stub(IProgressService, new TestProgressService()); + const workspaceContextService = new TestContextService(TestWorkspace); + instantiationService.stub(IWorkspaceContextService, workspaceContextService); + const configService = new TestConfigurationService(); + instantiationService.stub(IConfigurationService, configService); + instantiationService.stub(IFilesConfigurationService, new TestFilesConfigurationService(contextKeyService, configService, TestEnvironmentService)); + instantiationService.stub(ITextResourceConfigurationService, new TestTextResourceConfigurationService(configService)); + instantiationService.stub(IUntitledTextEditorService, instantiationService.createInstance(UntitledTextEditorService)); + instantiationService.stub(IStorageService, new TestStorageService()); + instantiationService.stub(IWorkbenchLayoutService, new TestLayoutService()); + instantiationService.stub(IDialogService, new TestDialogService()); + instantiationService.stub(IAccessibilityService, new TestAccessibilityService()); + instantiationService.stub(IFileDialogService, new TestFileDialogService()); + instantiationService.stub(IModeService, instantiationService.createInstance(ModeServiceImpl)); + instantiationService.stub(IHistoryService, new TestHistoryService()); + instantiationService.stub(ITextResourcePropertiesService, new TestTextResourcePropertiesService(configService)); + instantiationService.stub(IUndoRedoService, instantiationService.createInstance(UndoRedoService)); + instantiationService.stub(IModelService, instantiationService.createInstance(ModelServiceImpl)); + instantiationService.stub(IFileService, new TestFileService()); + instantiationService.stub(IBackupFileService, new TestBackupFileService()); + instantiationService.stub(ITelemetryService, NullTelemetryService); + instantiationService.stub(INotificationService, new TestNotificationService()); + instantiationService.stub(IUntitledTextEditorService, instantiationService.createInstance(UntitledTextEditorService)); + instantiationService.stub(IMenuService, new TestMenuService()); + instantiationService.stub(IKeybindingService, new MockKeybindingService()); + instantiationService.stub(IDecorationsService, new TestDecorationsService()); + instantiationService.stub(IExtensionService, new TestExtensionService()); + instantiationService.stub(IWorkingCopyFileService, instantiationService.createInstance(WorkingCopyFileService)); + instantiationService.stub(ITextFileService, overrides?.textFileService ? overrides.textFileService(instantiationService) : instantiationService.createInstance(TestTextFileService)); + instantiationService.stub(IHostService, instantiationService.createInstance(TestHostService)); + instantiationService.stub(ITextModelService, instantiationService.createInstance(TextModelResolverService)); + instantiationService.stub(IThemeService, new TestThemeService()); + instantiationService.stub(ILogService, new NullLogService()); + const editorGroupService = new TestEditorGroupsService([new TestEditorGroupView(0)]); + instantiationService.stub(IEditorGroupsService, editorGroupService); + instantiationService.stub(ILabelService, instantiationService.createInstance(LabelService)); + const editorService = new TestEditorService(editorGroupService); + instantiationService.stub(IEditorService, editorService); + instantiationService.stub(ICodeEditorService, new TestCodeEditorService()); + instantiationService.stub(IViewletService, new TestViewletService()); + + return instantiationService; +} + +export class TestServiceAccessor { + constructor( + @ILifecycleService public lifecycleService: TestLifecycleService, + @ITextFileService public textFileService: TestTextFileService, + @IWorkingCopyFileService public workingCopyFileService: IWorkingCopyFileService, + @IFilesConfigurationService public filesConfigurationService: TestFilesConfigurationService, + @IWorkspaceContextService public contextService: TestContextService, + @IModelService public modelService: ModelServiceImpl, + @IFileService public fileService: TestFileService, + @IFileDialogService public fileDialogService: TestFileDialogService, + @IWorkingCopyService public workingCopyService: IWorkingCopyService, + @IEditorService public editorService: TestEditorService, + @IEditorGroupsService public editorGroupService: IEditorGroupsService, + @IModeService public modeService: IModeService, + @ITextModelService public textModelResolverService: ITextModelService, + @IUntitledTextEditorService public untitledTextEditorService: UntitledTextEditorService, + @IConfigurationService public testConfigurationService: TestConfigurationService, + @IBackupFileService public backupFileService: TestBackupFileService + ) { } +} + export class TestTextFileService extends BrowserTextFileService { private resolveTextContentError!: FileOperationError | null; @@ -173,57 +251,6 @@ export class TestTextFileService extends BrowserTextFileService { export const TestEnvironmentService = new BrowserWorkbenchEnvironmentService(Object.create(null)); -export function workbenchInstantiationService(overrides?: { textFileService?: (instantiationService: IInstantiationService) => ITextFileService }): ITestInstantiationService { - const instantiationService = new TestInstantiationService(new ServiceCollection([ILifecycleService, new TestLifecycleService()])); - - instantiationService.stub(IWorkingCopyService, new TestWorkingCopyService()); - instantiationService.stub(IEnvironmentService, TestEnvironmentService); - const contextKeyService = instantiationService.createInstance(MockContextKeyService); - instantiationService.stub(IContextKeyService, contextKeyService); - instantiationService.stub(IProgressService, new TestProgressService()); - const workspaceContextService = new TestContextService(TestWorkspace); - instantiationService.stub(IWorkspaceContextService, workspaceContextService); - const configService = new TestConfigurationService(); - instantiationService.stub(IConfigurationService, configService); - instantiationService.stub(IFilesConfigurationService, new TestFilesConfigurationService(contextKeyService, configService, TestEnvironmentService)); - instantiationService.stub(ITextResourceConfigurationService, new TestTextResourceConfigurationService(configService)); - instantiationService.stub(IUntitledTextEditorService, instantiationService.createInstance(UntitledTextEditorService)); - instantiationService.stub(IStorageService, new TestStorageService()); - instantiationService.stub(IWorkbenchLayoutService, new TestLayoutService()); - instantiationService.stub(IDialogService, new TestDialogService()); - instantiationService.stub(IAccessibilityService, new TestAccessibilityService()); - instantiationService.stub(IFileDialogService, new TestFileDialogService()); - instantiationService.stub(IModeService, instantiationService.createInstance(ModeServiceImpl)); - instantiationService.stub(IHistoryService, new TestHistoryService()); - instantiationService.stub(ITextResourcePropertiesService, new TestTextResourcePropertiesService(configService)); - instantiationService.stub(IUndoRedoService, instantiationService.createInstance(UndoRedoService)); - instantiationService.stub(IModelService, instantiationService.createInstance(ModelServiceImpl)); - instantiationService.stub(IFileService, new TestFileService()); - instantiationService.stub(IBackupFileService, new TestBackupFileService()); - instantiationService.stub(ITelemetryService, NullTelemetryService); - instantiationService.stub(INotificationService, new TestNotificationService()); - instantiationService.stub(IUntitledTextEditorService, instantiationService.createInstance(UntitledTextEditorService)); - instantiationService.stub(IMenuService, new TestMenuService()); - instantiationService.stub(IKeybindingService, new MockKeybindingService()); - instantiationService.stub(IDecorationsService, new TestDecorationsService()); - instantiationService.stub(IExtensionService, new TestExtensionService()); - instantiationService.stub(IWorkingCopyFileService, instantiationService.createInstance(WorkingCopyFileService)); - instantiationService.stub(ITextFileService, overrides?.textFileService ? overrides.textFileService(instantiationService) : instantiationService.createInstance(TestTextFileService)); - instantiationService.stub(IHostService, instantiationService.createInstance(TestHostService)); - instantiationService.stub(ITextModelService, instantiationService.createInstance(TextModelResolverService)); - instantiationService.stub(IThemeService, new TestThemeService()); - instantiationService.stub(ILogService, new NullLogService()); - const editorGroupService = new TestEditorGroupsService([new TestEditorGroupView(0)]); - instantiationService.stub(IEditorGroupsService, editorGroupService); - instantiationService.stub(ILabelService, instantiationService.createInstance(LabelService)); - const editorService = new TestEditorService(editorGroupService); - instantiationService.stub(IEditorService, editorService); - instantiationService.stub(ICodeEditorService, new TestCodeEditorService()); - instantiationService.stub(IViewletService, new TestViewletService()); - - return instantiationService; -} - export class TestProgressService implements IProgressService { _serviceBrand: undefined; @@ -233,7 +260,7 @@ export class TestProgressService implements IProgressService { task: (progress: IProgress) => Promise, onDidCancel?: ((choice?: number | undefined) => void) | undefined ): Promise { - return task(emptyProgress); + return task(Progress.None); } } @@ -295,14 +322,7 @@ export class TestHistoryService implements IHistoryService { openLastEditLocation(): void { } } -export class TestDialogService implements IDialogService { - _serviceBrand: undefined; - - confirm(_confirmation: IConfirmation): Promise { return Promise.resolve({ confirmed: false }); } - show(_severity: Severity, _message: string, _buttons: string[], _options?: IDialogOptions): Promise { return Promise.resolve({ choice: 0 }); } - about(): Promise { return Promise.resolve(); } -} export class TestFileDialogService implements IFileDialogService { @@ -739,7 +759,14 @@ export class TestFileService implements IFileService { } del(_resource: URI, _options?: { useTrash?: boolean, recursive?: boolean }): Promise { return Promise.resolve(); } - watch(_resource: URI): IDisposable { return Disposable.None; } + + readonly watches: URI[] = []; + watch(_resource: URI): IDisposable { + this.watches.push(_resource); + + return toDisposable(() => this.watches.splice(this.watches.indexOf(_resource), 1)); + } + getWriteEncoding(_resource: URI): IResourceEncoding { return { encoding: 'utf8', hasBOM: false }; } dispose(): void { } } @@ -899,3 +926,136 @@ export class TestFilesConfigurationService extends FilesConfigurationService { super.onFilesConfigurationChange(configuration); } } + +export class TestReadonlyTextFileEditorModel extends TextFileEditorModel { + + isReadonly(): boolean { + return true; + } +} + +export class TestEditorInput extends EditorInput { + + constructor(public resource: URI, private typeId: string) { + super(); + } + + getTypeId(): string { + return this.typeId; + } + + resolve(): Promise { + return Promise.resolve(null); + } +} + +export function registerTestEditor(id: string, inputs: SyncDescriptor[], factoryInputId?: string): IDisposable { + class TestEditorControl extends BaseEditor { + + constructor() { super(id, NullTelemetryService, new TestThemeService(), new TestStorageService()); } + + async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + super.setInput(input, options, token); + + await input.resolve(); + } + + getId(): string { return id; } + layout(): void { } + createEditor(): any { } + } + + const disposables = new DisposableStore(); + + disposables.add(Registry.as(Extensions.Editors).registerEditor(EditorDescriptor.create(TestEditorControl, id, 'Test Editor Control'), inputs)); + + if (factoryInputId) { + + interface ISerializedTestInput { + resource: string; + } + + class EditorsObserverTestEditorInputFactory implements IEditorInputFactory { + + canSerialize(editorInput: EditorInput): boolean { + return true; + } + + serialize(editorInput: EditorInput): string { + let testEditorInput = editorInput; + let testInput: ISerializedTestInput = { + resource: testEditorInput.resource.toString() + }; + + return JSON.stringify(testInput); + } + + deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput { + let testInput: ISerializedTestInput = JSON.parse(serializedEditorInput); + + return new TestFileEditorInput(URI.parse(testInput.resource), factoryInputId!); + } + } + + disposables.add(Registry.as(EditorExtensions.EditorInputFactories).registerEditorInputFactory(factoryInputId, EditorsObserverTestEditorInputFactory)); + } + + return disposables; +} + +export class TestFileEditorInput extends EditorInput implements IFileEditorInput { + gotDisposed = false; + gotSaved = false; + gotSavedAs = false; + gotReverted = false; + dirty = false; + private fails = false; + + constructor( + public resource: URI, + private typeId: string + ) { + super(); + } + + getTypeId() { return this.typeId; } + resolve(): Promise { return !this.fails ? Promise.resolve(null) : Promise.reject(new Error('fails')); } + matches(other: TestEditorInput): boolean { return other && other.resource && this.resource.toString() === other.resource.toString() && other instanceof TestFileEditorInput && other.getTypeId() === this.typeId; } + setEncoding(encoding: string) { } + getEncoding() { return undefined; } + setPreferredEncoding(encoding: string) { } + setMode(mode: string) { } + setPreferredMode(mode: string) { } + setForceOpenAsBinary(): void { } + setFailToOpen(): void { + this.fails = true; + } + async save(groupId: GroupIdentifier, options?: ISaveOptions): Promise { + this.gotSaved = true; + return this; + } + async saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise { + this.gotSavedAs = true; + return this; + } + async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { + this.gotReverted = true; + this.gotSaved = false; + this.gotSavedAs = false; + return true; + } + setDirty(): void { this.dirty = true; } + isDirty(): boolean { + return this.dirty; + } + isReadonly(): boolean { + return false; + } + isResolved(): boolean { return false; } + dispose(): void { + super.dispose(); + this.gotDisposed = true; + } + movedEditor: IMoveResult | undefined = undefined; + move(): IMoveResult | undefined { return this.movedEditor; } +} diff --git a/src/vs/workbench/test/common/notifications.test.ts b/src/vs/workbench/test/common/notifications.test.ts index 8984e0ab869..7eefd25cb8e 100644 --- a/src/vs/workbench/test/common/notifications.test.ts +++ b/src/vs/workbench/test/common/notifications.test.ts @@ -23,6 +23,7 @@ suite('Notifications', () => { let item3 = NotificationViewItem.create({ severity: Severity.Info, message: 'Info Message' })!; let item4 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message', source: 'Source' })!; let item5 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message', actions: { primary: [new Action('id', 'label')] } })!; + let item6 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message', actions: { primary: [new Action('id', 'label')] }, progress: { infinite: true } })!; assert.equal(item1.equals(item1), true); assert.equal(item2.equals(item2), true); @@ -35,6 +36,10 @@ suite('Notifications', () => { assert.equal(item1.equals(item4), false); assert.equal(item1.equals(item5), false); + // Progress + assert.equal(item1.hasProgress, false); + assert.equal(item6.hasProgress, true); + // Message Box assert.equal(item5.canCollapse, false); assert.equal(item5.expanded, true); @@ -102,8 +107,8 @@ suite('Notifications', () => { assert.equal(called, 1); // Error with Action - let item6 = NotificationViewItem.create({ severity: Severity.Error, message: createErrorWithActions('Hello Error', { actions: [new Action('id', 'label')] }) })!; - assert.equal(item6.actions!.primary!.length, 1); + let item7 = NotificationViewItem.create({ severity: Severity.Error, message: createErrorWithActions('Hello Error', { actions: [new Action('id', 'label')] }) })!; + assert.equal(item7.actions!.primary!.length, 1); // Filter let item8 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message' }, NotificationsFilter.SILENT)!; diff --git a/src/vs/workbench/test/electron-browser/quickopen.perf.integrationTest.ts b/src/vs/workbench/test/electron-browser/quickopen.perf.integrationTest.ts index 2c5b03c33ed..4af6351e22c 100644 --- a/src/vs/workbench/test/electron-browser/quickopen.perf.integrationTest.ts +++ b/src/vs/workbench/test/electron-browser/quickopen.perf.integrationTest.ts @@ -34,6 +34,11 @@ import { ClassifiedEvent, StrictPropertyCheck, GDPRClassification } from 'vs/pla import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { NullLogService } from 'vs/platform/log/common/log'; import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; +import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; +import { INotificationService } from 'vs/platform/notification/common/notification'; namespace Timer { export interface ITimerEvent { @@ -73,11 +78,17 @@ suite.skip('QuickOpen performance (integration)', () => { const telemetryService = new TestTelemetryService(); const configurationService = new TestConfigurationService(); const textResourcePropertiesService = new TestTextResourcePropertiesService(configurationService); + const dialogService = new TestDialogService(); + const notificationService = new TestNotificationService(); + const undoRedoService = new UndoRedoService(dialogService, notificationService); const instantiationService = new InstantiationService(new ServiceCollection( [ITelemetryService, telemetryService], [IConfigurationService, configurationService], [ITextResourcePropertiesService, textResourcePropertiesService], - [IModelService, new ModelServiceImpl(configurationService, textResourcePropertiesService, new TestThemeService(), new NullLogService(), new UndoRedoService())], + [IDialogService, dialogService], + [INotificationService, notificationService], + [IUndoRedoService, undoRedoService], + [IModelService, new ModelServiceImpl(configurationService, textResourcePropertiesService, new TestThemeService(), new NullLogService(), undoRedoService)], [IWorkspaceContextService, new TestContextService(testWorkspace(URI.file(testWorkspacePath)))], [IEditorService, new TestEditorService()], [IEditorGroupsService, new TestEditorGroupsService()], diff --git a/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts b/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts index 4ccd31573b5..be6ef9e5492 100644 --- a/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts +++ b/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts @@ -37,6 +37,11 @@ import { ITextResourcePropertiesService } from 'vs/editor/common/services/textRe import { ClassifiedEvent, StrictPropertyCheck, GDPRClassification } from 'vs/platform/telemetry/common/gdprTypings'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; +import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; +import { INotificationService } from 'vs/platform/notification/common/notification'; // declare var __dirname: string; @@ -63,11 +68,17 @@ suite.skip('TextSearch performance (integration)', () => { const configurationService = new TestConfigurationService(); const textResourcePropertiesService = new TestTextResourcePropertiesService(configurationService); const logService = new NullLogService(); + const dialogService = new TestDialogService(); + const notificationService = new TestNotificationService(); + const undoRedoService = new UndoRedoService(dialogService, notificationService); const instantiationService = new InstantiationService(new ServiceCollection( [ITelemetryService, telemetryService], [IConfigurationService, configurationService], [ITextResourcePropertiesService, textResourcePropertiesService], - [IModelService, new ModelServiceImpl(configurationService, textResourcePropertiesService, new TestThemeService(), logService, new UndoRedoService())], + [IDialogService, dialogService], + [INotificationService, notificationService], + [IUndoRedoService, undoRedoService], + [IModelService, new ModelServiceImpl(configurationService, textResourcePropertiesService, new TestThemeService(), logService, undoRedoService)], [IWorkspaceContextService, new TestContextService(testWorkspace(URI.file(testWorkspacePath)))], [IEditorService, new TestEditorService()], [IEditorGroupsService, new TestEditorGroupsService()], diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index 769dfdcb2ce..929d1d748ad 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { workbenchInstantiationService as browserWorkbenchInstantiationService, ITestInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { workbenchInstantiationService as browserWorkbenchInstantiationService, ITestInstantiationService, TestLifecycleService, TestFilesConfigurationService, TestContextService, TestFileService, TestFileDialogService } from 'vs/workbench/test/browser/workbenchTestServices'; import { Event } from 'vs/base/common/event'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; -import { NativeTextFileService } from 'vs/workbench/services/textfile/electron-browser/nativeTextFileService'; +import { NativeTextFileService, EncodingOracle, IEncodingOverride } from 'vs/workbench/services/textfile/electron-browser/nativeTextFileService'; import { IElectronService } from 'vs/platform/electron/node/electron'; import { INativeOpenDialogOptions } from 'vs/platform/dialogs/node/dialogs'; import { FileOperationError, IFileService } from 'vs/platform/files/common/files'; @@ -30,6 +30,13 @@ import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv'; import { LogLevel } from 'vs/platform/log/common/log'; import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService'; import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { UTF16le, UTF16be, UTF8_with_bom } from 'vs/base/node/encoding'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; +import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { NodeTestBackupFileService } from 'vs/workbench/services/backup/test/electron-browser/backupFileService.test'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export const TestWindowConfiguration: IWindowConfiguration = { windowId: 0, @@ -110,6 +117,31 @@ export class TestTextFileService extends NativeTextFileService { } } +export class TestNativeTextFileServiceWithEncodingOverrides extends NativeTextFileService { + + private _testEncoding: TestEncodingOracle | undefined; + get encoding(): TestEncodingOracle { + if (!this._testEncoding) { + this._testEncoding = this._register(this.instantiationService.createInstance(TestEncodingOracle)); + } + + return this._testEncoding; + } +} + +class TestEncodingOracle extends EncodingOracle { + + protected get encodingOverrides(): IEncodingOverride[] { + return [ + { extension: 'utf16le', encoding: UTF16le }, + { extension: 'utf16be', encoding: UTF16be }, + { extension: 'utf8bom', encoding: UTF8_with_bom } + ]; + } + + protected set encodingOverrides(overrides: IEncodingOverride[]) { } +} + export class TestSharedProcessService implements ISharedProcessService { _serviceBrand: undefined; @@ -187,3 +219,20 @@ export function workbenchInstantiationService(): ITestInstantiationService { return instantiationService; } + +export class TestServiceAccessor { + constructor( + @ILifecycleService public lifecycleService: TestLifecycleService, + @ITextFileService public textFileService: TestTextFileService, + @IFilesConfigurationService public filesConfigurationService: TestFilesConfigurationService, + @IWorkspaceContextService public contextService: TestContextService, + @IModelService public modelService: ModelServiceImpl, + @IFileService public fileService: TestFileService, + @IElectronService public electronService: TestElectronService, + @IFileDialogService public fileDialogService: TestFileDialogService, + @IBackupFileService public backupFileService: NodeTestBackupFileService, + @IWorkingCopyService public workingCopyService: IWorkingCopyService, + @IEditorService public editorService: IEditorService + ) { + } +} diff --git a/test/automation/package.json b/test/automation/package.json index a3f3baf2f40..8fced3ba824 100644 --- a/test/automation/package.json +++ b/test/automation/package.json @@ -28,7 +28,8 @@ "concurrently": "^3.5.1", "cpx": "^1.5.0", "typescript": "3.7.5", - "watch": "^1.0.2" + "watch": "^1.0.2", + "tree-kill": "1.2.2" }, "dependencies": { "mkdirp": "^0.5.1", diff --git a/test/automation/src/code.ts b/test/automation/src/code.ts index 3b5a667f81f..34bd09b739e 100644 --- a/test/automation/src/code.ts +++ b/test/automation/src/code.ts @@ -115,9 +115,6 @@ async function createDriverHandle(): Promise { } export async function spawn(options: SpawnOptions): Promise { - const codePath = options.codePath; - const electronPath = codePath ? getBuildElectronPath(codePath) : getDevElectronPath(); - const outPath = codePath ? getBuildOutPath(codePath) : getDevOutPath(); const handle = await createDriverHandle(); let child: cp.ChildProcess | undefined; @@ -126,63 +123,67 @@ export async function spawn(options: SpawnOptions): Promise { if (options.web) { await launch(options.userDataDir, options.workspacePath, options.codePath); connectDriver = connectPlaywrightDriver.bind(connectPlaywrightDriver, options.browser); - } else { - const env = process.env; - - const args = [ - options.workspacePath, - '--skip-getting-started', - '--skip-release-notes', - '--sticky-quickopen', - '--disable-telemetry', - '--disable-updates', - '--disable-crash-reporter', - `--extensions-dir=${options.extensionsPath}`, - `--user-data-dir=${options.userDataDir}`, - '--driver', handle - ]; - - if (options.remote) { - // Replace workspace path with URI - args[0] = `--${options.workspacePath.endsWith('.code-workspace') ? 'file' : 'folder'}-uri=vscode-remote://test+test/${URI.file(options.workspacePath).path}`; - - if (codePath) { - // running against a build: copy the test resolver extension - const testResolverExtPath = path.join(options.extensionsPath, 'vscode-test-resolver'); - if (!fs.existsSync(testResolverExtPath)) { - const orig = path.join(repoPath, 'extensions', 'vscode-test-resolver'); - await new Promise((c, e) => ncp(orig, testResolverExtPath, err => err ? e(err) : c())); - } - } - args.push('--enable-proposed-api=vscode.vscode-test-resolver'); - const remoteDataDir = `${options.userDataDir}-server`; - mkdirp.sync(remoteDataDir); - env['TESTRESOLVER_DATA_FOLDER'] = remoteDataDir; - } - - if (!codePath) { - args.unshift(repoPath); - } - - if (options.verbose) { - args.push('--driver-verbose'); - } - - if (options.log) { - args.push('--log', options.log); - } - - if (options.extraArgs) { - args.push(...options.extraArgs); - } - - const spawnOptions: cp.SpawnOptions = { env }; - child = cp.spawn(electronPath, args, spawnOptions); - instances.add(child); - child.once('exit', () => instances.delete(child!)); - connectDriver = connectElectronDriver; + return connect(connectDriver, child, '', handle, options.logger); } + const env = process.env; + const codePath = options.codePath; + const outPath = codePath ? getBuildOutPath(codePath) : getDevOutPath(); + + const args = [ + options.workspacePath, + '--skip-getting-started', + '--skip-release-notes', + '--sticky-quickopen', + '--disable-telemetry', + '--disable-updates', + '--disable-crash-reporter', + `--extensions-dir=${options.extensionsPath}`, + `--user-data-dir=${options.userDataDir}`, + `--disable-restore-windows`, + '--driver', handle + ]; + + if (options.remote) { + // Replace workspace path with URI + args[0] = `--${options.workspacePath.endsWith('.code-workspace') ? 'file' : 'folder'}-uri=vscode-remote://test+test/${URI.file(options.workspacePath).path}`; + + if (codePath) { + // running against a build: copy the test resolver extension + const testResolverExtPath = path.join(options.extensionsPath, 'vscode-test-resolver'); + if (!fs.existsSync(testResolverExtPath)) { + const orig = path.join(repoPath, 'extensions', 'vscode-test-resolver'); + await new Promise((c, e) => ncp(orig, testResolverExtPath, err => err ? e(err) : c())); + } + } + args.push('--enable-proposed-api=vscode.vscode-test-resolver'); + const remoteDataDir = `${options.userDataDir}-server`; + mkdirp.sync(remoteDataDir); + env['TESTRESOLVER_DATA_FOLDER'] = remoteDataDir; + } + + if (!codePath) { + args.unshift(repoPath); + } + + if (options.verbose) { + args.push('--driver-verbose'); + } + + if (options.log) { + args.push('--log', options.log); + } + + if (options.extraArgs) { + args.push(...options.extraArgs); + } + + const electronPath = codePath ? getBuildElectronPath(codePath) : getDevElectronPath(); + const spawnOptions: cp.SpawnOptions = { env }; + child = cp.spawn(electronPath, args, spawnOptions); + instances.add(child); + child.once('exit', () => instances.delete(child!)); + connectDriver = connectElectronDriver; return connect(connectDriver, child, outPath, handle, options.logger); } diff --git a/test/automation/src/playwrightDriver.ts b/test/automation/src/playwrightDriver.ts index 40ee4c82851..cc0c5547db3 100644 --- a/test/automation/src/playwrightDriver.ts +++ b/test/automation/src/playwrightDriver.ts @@ -10,6 +10,7 @@ import { mkdir } from 'fs'; import { promisify } from 'util'; import { IDriver, IDisposable } from './driver'; import { URI } from 'vscode-uri'; +import * as kill from 'tree-kill'; const width = 1200; const height = 800; @@ -93,6 +94,7 @@ let workspacePath: string | undefined; export async function launch(userDataDir: string, _workspacePath: string, codeServerPath = process.env.VSCODE_REMOTE_SERVER_PATH): Promise { workspacePath = _workspacePath; + const agentFolder = userDataDir; await promisify(mkdir)(agentFolder); const env = { @@ -121,7 +123,7 @@ export async function launch(userDataDir: string, _workspacePath: string, codeSe function teardown(): void { if (server) { - server.kill(); + kill(server.pid); server = undefined; } } @@ -137,13 +139,9 @@ function waitForEndpoint(): Promise { }); } -export function connect(engine: 'chromium' | 'webkit' | 'firefox' = 'chromium'): Promise<{ client: IDisposable, driver: IDriver }> { +export function connect(browserType: 'chromium' | 'webkit' | 'firefox' = 'chromium'): Promise<{ client: IDisposable, driver: IDriver }> { return new Promise(async (c) => { - const browser = await playwright[engine].launch({ - // Run in Edge dev on macOS - // executablePath: '/Applications/Microsoft\ Edge\ Dev.app/Contents/MacOS/Microsoft\ Edge\ Dev', - headless: false - }); + const browser = await playwright[browserType].launch({ headless: false, dumpio: true }); const context = await browser.newContext(); const page = await context.newPage(); await page.setViewportSize({ width, height }); diff --git a/test/automation/yarn.lock b/test/automation/yarn.lock index 98c63c91e0d..11785373d40 100644 --- a/test/automation/yarn.lock +++ b/test/automation/yarn.lock @@ -1634,6 +1634,11 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" +tree-kill@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" + integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== + tree-kill@^1.1.0: version "1.2.1" resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.1.tgz#5398f374e2f292b9dcc7b2e71e30a5c3bb6c743a" diff --git a/test/smoke/src/areas/workbench/localization.test.ts b/test/smoke/src/areas/workbench/localization.test.ts index 5db5fe2b74a..07e9199080e 100644 --- a/test/smoke/src/areas/workbench/localization.test.ts +++ b/test/smoke/src/areas/workbench/localization.test.ts @@ -37,10 +37,10 @@ export function setup() { await app.workbench.scm.waitForTitle(title => /quellcodeverwaltung/i.test(title)); await app.workbench.debug.openDebugViewlet(); - await app.workbench.debug.waitForTitle(title => /debug/i.test(title)); + await app.workbench.debug.waitForTitle(title => /starten/i.test(title)); - // await app.workbench.extensions.openExtensionsViewlet(); - // await app.workbench.extensions.waitForTitle(title => /erweiterungen/i.test(title)); + await app.workbench.extensions.openExtensionsViewlet(); + await app.workbench.extensions.waitForTitle(title => /extensions/i.test(title)); }); }); } diff --git a/test/smoke/src/main.ts b/test/smoke/src/main.ts index 8122d235289..a1273ba379d 100644 --- a/test/smoke/src/main.ts +++ b/test/smoke/src/main.ts @@ -57,8 +57,7 @@ const opts = minimist(args, { boolean: [ 'verbose', 'remote', - 'web', - 'ci' + 'web' ], default: { verbose: false @@ -299,24 +298,16 @@ describe(`VSCode Smoke Tests (${opts.web ? 'Web' : 'Electron'})`, () => { }); } - // CI only tests (must be reliable) - if (opts.ci) { - // TODO@Ben figure out tests that can run continously and reliably - } - - // Non-CI execution (all tests) - else { - if (!opts.web) { setupDataMigrationTests(opts['stable-build'], testDataPath); } - if (!opts.web) { setupDataLossTests(); } - if (!opts.web) { setupDataPreferencesTests(); } - setupDataSearchTests(); - setupDataCSSTests(); - setupDataEditorTests(); - setupDataStatusbarTests(!!opts.web); - if (!opts.web) { setupDataExtensionTests(); } - setupTerminalTests(); - if (!opts.web) { setupDataMultirootTests(); } - if (!opts.web) { setupDataLocalizationTests(); } - if (!opts.web) { setupLaunchTests(); } - } + if (!opts.web) { setupDataMigrationTests(opts['stable-build'], testDataPath); } + if (!opts.web) { setupDataLossTests(); } + if (!opts.web) { setupDataPreferencesTests(); } + setupDataSearchTests(); + setupDataCSSTests(); + setupDataEditorTests(); + setupDataStatusbarTests(!!opts.web); + if (!opts.web) { setupDataExtensionTests(); } + setupTerminalTests(); + if (!opts.web) { setupDataMultirootTests(); } + if (!opts.web) { setupDataLocalizationTests(); } + if (!opts.web) { setupLaunchTests(); } }); diff --git a/yarn.lock b/yarn.lock index ec7e792caac..a820c6344ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -614,6 +614,11 @@ ajv-keywords@^3.1.0: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a" integrity sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo= +ajv-keywords@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da" + integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ== + ajv@^5.1.0: version "5.3.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.3.0.tgz#4414ff74a50879c208ee5fdc826e32c303549eda" @@ -1515,7 +1520,7 @@ camelcase@^3.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" integrity sha1-MvxLn82vhF/N9+c7uXysImHwqwo= -camelcase@^5.0.0: +camelcase@^5.0.0, camelcase@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== @@ -2199,6 +2204,24 @@ css-color-names@0.0.4: resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" integrity sha1-gIrcLnnPhHOAabZGyyDsJ762KeA= +css-loader@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.2.0.tgz#bb570d89c194f763627fcf1f80059c6832d009b2" + integrity sha512-QTF3Ud5H7DaZotgdcJjGMvyDj5F3Pn1j/sC6VBEOVp94cbwqyIBdcs/quzj4MC1BKQSrTpQznegH/5giYbhnCQ== + dependencies: + camelcase "^5.3.1" + cssesc "^3.0.0" + icss-utils "^4.1.1" + loader-utils "^1.2.3" + normalize-path "^3.0.0" + postcss "^7.0.17" + postcss-modules-extract-imports "^2.0.0" + postcss-modules-local-by-default "^3.0.2" + postcss-modules-scope "^2.1.0" + postcss-modules-values "^3.0.0" + postcss-value-parser "^4.0.0" + schema-utils "^2.0.0" + css-select@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" @@ -2214,6 +2237,11 @@ css-what@2.1: resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.0.tgz#9467d032c38cfaefb9f2d79501253062f87fa1bd" integrity sha1-lGfQMsOM+u+58teVASUwYvh/ob0= +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + cssnano@^3.0.0: version "3.10.0" resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-3.10.0.tgz#4f38f6cea2b9b17fa01490f23f1dc68ea65c1c38" @@ -3272,6 +3300,14 @@ file-entry-cache@^5.0.1: dependencies: flat-cache "^2.0.1" +file-loader@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-4.2.0.tgz#5fb124d2369d7075d70a9a5abecd12e60a95215e" + integrity sha512-+xZnaK5R8kBJrHK0/6HRlrKNamvVS5rjyuju+rnyxRGuwUJwpAMsVzUl5dz6rK8brkzjV6JpcFNjp6NqV0g1OQ== + dependencies: + loader-utils "^1.2.3" + schema-utils "^2.0.0" + file-uri-to-path@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" @@ -4523,6 +4559,13 @@ iconv-lite@^0.4.4: dependencies: safer-buffer ">= 2.1.2 < 3" +icss-utils@^4.0.0, icss-utils@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467" + integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA== + dependencies: + postcss "^7.0.14" + ieee754@^1.1.11, ieee754@^1.1.4: version "1.1.12" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b" @@ -5482,7 +5525,7 @@ loader-runner@^2.3.0: resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" integrity sha1-9IKuqC1UPgeSFwDVpG7yb9rGuKI= -loader-utils@1.2.3: +loader-utils@1.2.3, loader-utils@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== @@ -7124,6 +7167,39 @@ postcss-minify-selectors@^2.0.4: postcss "^5.0.14" postcss-selector-parser "^2.0.0" +postcss-modules-extract-imports@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e" + integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ== + dependencies: + postcss "^7.0.5" + +postcss-modules-local-by-default@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.2.tgz#e8a6561be914aaf3c052876377524ca90dbb7915" + integrity sha512-jM/V8eqM4oJ/22j0gx4jrp63GSvDH6v86OqyTHHUvk4/k1vceipZsaymiZ5PvocqZOl5SFHiFJqjs3la0wnfIQ== + dependencies: + icss-utils "^4.1.1" + postcss "^7.0.16" + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.0.0" + +postcss-modules-scope@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.1.0.tgz#ad3f5bf7856114f6fcab901b0502e2a2bc39d4eb" + integrity sha512-91Rjps0JnmtUB0cujlc8KIKCsJXWjzuxGeT/+Q2i2HXKZ7nBUeF9YQTZZTNvHVoNYj1AthsjnGLtqDUE0Op79A== + dependencies: + postcss "^7.0.6" + postcss-selector-parser "^6.0.0" + +postcss-modules-values@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10" + integrity sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg== + dependencies: + icss-utils "^4.0.0" + postcss "^7.0.6" + postcss-normalize-charset@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz#ef9ee71212d7fe759c78ed162f61ed62b5cb93f1" @@ -7182,6 +7258,15 @@ postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2: indexes-of "^1.0.1" uniq "^1.0.1" +postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c" + integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg== + dependencies: + cssesc "^3.0.0" + indexes-of "^1.0.1" + uniq "^1.0.1" + postcss-svgo@^2.1.1: version "2.1.6" resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-2.1.6.tgz#b6df18aa613b666e133f08adb5219c2684ac108d" @@ -7206,6 +7291,11 @@ postcss-value-parser@^3.0.1, postcss-value-parser@^3.0.2, postcss-value-parser@^ resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15" integrity sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU= +postcss-value-parser@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz#482282c09a42706d1fc9a069b73f44ec08391dc9" + integrity sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ== + postcss-zindex@^2.0.1: version "2.2.0" resolved "https://registry.yarnpkg.com/postcss-zindex/-/postcss-zindex-2.2.0.tgz#d2109ddc055b91af67fc4cb3b025946639d2af22" @@ -7225,6 +7315,15 @@ postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0 source-map "^0.5.6" supports-color "^3.2.3" +postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.17, postcss@^7.0.5, postcss@^7.0.6: + version "7.0.21" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.21.tgz#06bb07824c19c2021c5d056d5b10c35b989f7e17" + integrity sha512-uIFtJElxJo29QC753JzhidoAhvp/e/Exezkdhfmt8AymWT6/5B7W1WmponYWkHk2eg6sONyTch0A3nkMPun3SQ== + dependencies: + chalk "^2.4.2" + source-map "^0.6.1" + supports-color "^6.1.0" + prebuild-install@5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-5.3.0.tgz#58b4d8344e03590990931ee088dd5401b03004c8" @@ -8063,6 +8162,14 @@ schema-utils@^0.4.4, schema-utils@^0.4.5: ajv "^6.1.0" ajv-keywords "^3.1.0" +schema-utils@^2.0.0, schema-utils@^2.0.1: + version "2.5.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.5.0.tgz#8f254f618d402cc80257486213c8970edfd7c22f" + integrity sha512-32ISrwW2scPXHUSusP8qMg5dLUawKkyV+/qIEV9JdXKx+rsM6mi8vZY8khg2M69Qom16rtroWXD3Ybtiws38gQ== + dependencies: + ajv "^6.10.2" + ajv-keywords "^3.4.1" + semver-compare@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" @@ -8660,6 +8767,14 @@ strip-json-comments@^3.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== +style-loader@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-1.0.0.tgz#1d5296f9165e8e2c85d24eee0b7caf9ec8ca1f82" + integrity sha512-B0dOCFwv7/eY31a5PCieNwMgMhVGFe9w+rh7s/Bx8kfFkrth9zfTZquoYvdw8URgiqxObQKcpW51Ugz1HjfdZw== + dependencies: + loader-utils "^1.2.3" + schema-utils "^2.0.1" + sudo-prompt@9.1.1: version "9.1.1" resolved "https://registry.yarnpkg.com/sudo-prompt/-/sudo-prompt-9.1.1.tgz#73853d729770392caec029e2470db9c221754db0" @@ -8677,7 +8792,7 @@ supports-color@1.2.0: resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-1.2.0.tgz#ff1ed1e61169d06b3cf2d588e188b18d8847e17e" integrity sha1-/x7R5hFp0Gs88tWI4YixjYhH4X4= -supports-color@6.1.0: +supports-color@6.1.0, supports-color@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== @@ -9523,10 +9638,10 @@ vsce@1.48.0: yauzl "^2.3.1" yazl "^2.2.2" -vscode-debugprotocol@1.37.0: - version "1.37.0" - resolved "https://registry.yarnpkg.com/vscode-debugprotocol/-/vscode-debugprotocol-1.37.0.tgz#e8c4694a078d18ea1a639553a7a241b35c1e6f6d" - integrity sha512-ppZLEBbFRVNsK0YpfgFi/x2CDyihx0F+UpdKmgeJcvi05UgSXYdO0n9sDVYwoGvvYQPvlpDQeWuY0nloOC4mPA== +vscode-debugprotocol@1.39.0: + version "1.39.0" + resolved "https://registry.yarnpkg.com/vscode-debugprotocol/-/vscode-debugprotocol-1.39.0.tgz#0c639178d0d5ea7de7903b6478b53d2bc0d77461" + integrity sha512-Wkvgtuz90vjtQBcvw9Z+BYa4dA6W+sHwHMpqvJVNmwWSuT3JZdl0XDhZNLqtMXkVF4okxtAe0MmbupPSt+gnAQ== vscode-minimist@^1.2.2: version "1.2.2"