diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000000..dda0884b38 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,16 @@ +**/vs/nls.build.js +**/vs/nls.js +**/vs/css.build.js +**/vs/css.js +**/vs/loader.js +**/promise-polyfill/** +**/insane/** +**/marked/** +**/test/**/*.js +**/node_modules/** +**/vscode-api-tests/testWorkspace/** +**/vscode-api-tests/testWorkspace2/** +**/extensions/**/out/** +**/extensions/**/build/** +**/extensions/markdown-language-features/media/** +**/extensions/typescript-basics/test/colorize-fixtures/** diff --git a/.eslintrc.json b/.eslintrc.json index 29efa7cbbc..c80eb53cd6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,20 +1,273 @@ { - "root": true, - "env": { - "node": true, - "es6": true - }, - "rules": { - "no-console": 0, - "no-cond-assign": 0, - "no-unused-vars": 1, - "no-extra-semi": "warn", - "semi": "warn" - }, - "extends": "eslint:recommended", - "parserOptions": { - "ecmaFeatures": { - "experimentalObjectRestSpread": true - } - } + "root": true, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint", + "jsdoc" + ], + "rules": { + "constructor-super": "warn", + "curly": "warn", + "eqeqeq": "warn", + "no-buffer-constructor": "warn", + "no-caller": "warn", + "no-debugger": "warn", + "no-duplicate-case": "warn", + "no-duplicate-imports": "warn", + "no-eval": "warn", + "no-extra-semi": "warn", + "no-new-wrappers": "warn", + "no-redeclare": "off", + "no-sparse-arrays": "warn", + "no-throw-literal": "warn", + "no-unsafe-finally": "warn", + "no-unused-labels": "warn", + "no-restricted-globals": ["warn", "name", "length", "event", "closed", "external", "status", "origin", "orientation"], // non-complete list of globals that are easy to access unintentionally + "no-var": "warn", + "jsdoc/no-types": "warn", + "semi": "off", + "@typescript-eslint/semi": "warn", + "@typescript-eslint/class-name-casing": "warn", + "code-no-unused-expressions": [ + "warn", + { + "allowTernary": true + } + ], + "code-translation-remind": "warn", + "code-no-nls-in-standalone-editor": "warn", + "code-no-standalone-editor": "warn", + "code-no-unexternalized-strings": "warn", + "code-layering": [ + "warn", + { + "common": ["browser"], // {{SQL CARBON EDIT}} @anthonydresser not ideal, but for our purposes its fine for now, + "node": [ + "common" + ], + "browser": [ + "common" + ], + "electron-main": [ + "common", + "node" + ], + "electron-browser": [ + "common", + "browser", + "node" + ] + } + ], + "code-import-patterns": [ + "warn", + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // !!! Do not relax these rules !!! + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + { + "target": "**/{vs,sql}/base/common/**", + "restrictions": [ + "vs/nls", + "**/{vs,sql}/base/common/**" + ] + }, + { + "target": "**/{vs,sql}/base/test/common/**", + "restrictions": [ + "assert", + "sinon", + "vs/nls", + "**/{vs,sql}/base/common/**", + "**/{vs,sql}/base/test/common/**" + ] + }, + { + "target": "**/{vs,sql}/base/browser/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/{vs,sql}/base/{common,browser}/**", + "@angular/*", + "rxjs/*" + ] + }, + { + "target": "**/{vs,sql}/base/node/**", + "restrictions": [ + "vs/nls", + "**/{vs,sql}/base/{common,browser,node}/**", + "!path" // node modules (except path where we have our own impl) + ] + }, + { + // vs/base/test/browser contains tests for vs/base/browser + "target": "**/{vs,sql}/base/test/browser/**", + "restrictions": [ + "assert", + "vs/nls", + "**/{vs,sql}/base/{common,browser}/**", + "**/{vs,sql}/base/test/{common,browser}/**", + "@angular/*", + "rxjs/*" + ] + }, + { + "target": "**/{vs,sql}/base/parts/*/common/**", + "restrictions": [ + "vs/nls", + "**/{vs,sql}/base/common/**", + "**/{vs,sql}/base/parts/*/common/**" + ] + }, + { + "target": "**/{vs,sql}/base/parts/*/browser/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/{vs,sql}/base/{common,browser}/**", + "**/{vs,sql}/base/parts/*/{common,browser}/**", + "@angular/*", + "rxjs/*" + ] + }, + { + "target": "**/{vs,sql}/base/parts/*/node/**", + "restrictions": [ + "vs/nls", + "**/{vs,sql}/base/{common,browser,node}/**", + "**/{vs,sql}/base/parts/*/{common,browser,node}/**", + "!path" // node modules (except path where we have our own impl) + ] + }, + { + "target": "**/{vs,sql}/base/parts/*/electron-browser/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/{vs,sql}/base/{common,browser,node,electron-browser}/**", + "**/{vs,sql}/base/parts/*/{common,browser,node,electron-browser}/**", + "!path", // node modules (except path where we have our own impl) + "@angular/*", + "rxjs/*" + ] + }, + { + "target": "**/{vs,sql}/base/parts/*/electron-main/**", + "restrictions": [ + "vs/nls", + "**/{vs,sql}/base/{common,browser,node,electron-main}/**", + "**/{vs,sql}/base/parts/*/{common,browser,node,electron-main}/**", + "!path", // node modules (except path where we have our own impl) + "@angular/*", + "rxjs/*" + ] + }, + { + "target": "**/{vs,sql}/platform/*/common/**", + "restrictions": [ + "vs/nls", + "**/{vs,sql}/base/common/**", + "**/{vs,sql}/base/parts/*/common/**", + "**/{vs,sql}/{platform,workbench}/**/common/**", + "**/vs/editor/common/**", + "azdata" // TODO remove + ] + }, + { + "target": "**/{vs,sql}/platform/*/test/common/**", + "restrictions": [ + "assert", + "sinon", + "vs/nls", + "**/{vs,sql}/base/common/**", + "**/{vs,sql}/{platform,workbench}/**/common/**", + "**/{vs,sql}/{base,platform,workbench}/**/test/common/**", + "typemoq", + "azdata" // TODO remove + ] + }, + { + "target": "**/{vs,sql}/platform/*/browser/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/{vs,sql}/base/{common,browser}/**", + "**/{vs,sql}/base/parts/*/{common,browser}/**", + "**/{vs,sql}/{platform,workbench}/**/{common,browser}/**", + "**/vs/editor/{common,browser}/**", + "@angular/*", + "rxjs/*", + "azdata" // TODO remove + ] + }, + { + "target": "**/{vs,sql}/platform/*/node/**", + "restrictions": [ + "vs/nls", + "**/{vs,sql}/base/{common,browser,node}/**", + "**/{vs,sql}/base/parts/*/{common,browser,node}/**", + "**/{vs,sql}/{platform,workbench}/**/{common,browser,node}/**", + "**/vs/editor/{common,browser,node}/**", + "!path", // node modules (except path where we have our own impl) + "azdata" // TODO remove + ] + }, + { + "target": "**/{vs,sql}/platform/*/electron-browser/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/{vs,sql}/base/{common,browser,node}/**", + "**/{vs,sql}/base/parts/*/{common,browser,node,electron-browser}/**", + "**/{vs,sql}/{platform,workbench}/**/{common,browser,node,electron-browser}/**", + "**/vs/editor/{common,browser,node,electron-browser}/**", + "!path", // node modules (except path where we have our own impl) + "@angular/*", + "rxjs/*", + "azdata" // TODO remove + ] + }, + { + "target": "**/{vs,sql}/platform/*/electron-main/**", + "restrictions": [ + "vs/nls", + "**/{vs,sql}/base/{common,browser,node}/**", + "**/{vs,sql}/base/parts/*/{common,browser,node,electron-browser}/**", + "**/{vs,sql}/{platform,workbench}/**/{common,browser,node,electron-main}/**", + "**/vs/editor/{common,browser,node,electron-main}/**", + "!path", // node modules (except path where we have our own impl) + "azdata" // TODO remove + ] + }, + { + "target": "**/{vs,sql}/platform/*/test/browser/**", + "restrictions": [ + "assert", + "sinon", + "vs/nls", + "**/{vs,sql}/base/{common,browser}/**", + "**/{vs,sql}/{platform,workbench}/**/{common,browser}/**", + "**/{vs,sql}/{base,platform,workbench}/**/test/{common,browser}/**", + "typemoq", + "@angular/*", + "rxjs/*", + "azdata" // TODO remove + ] + } + ] + }, + "overrides": [ + { + "files": [ + "*.js" + ], + "rules": { + "jsdoc/no-types": "off" + } + } + ] } diff --git a/.eslintrc.sql.json b/.eslintrc.sql.json new file mode 100644 index 0000000000..952c2b454c --- /dev/null +++ b/.eslintrc.sql.json @@ -0,0 +1,7 @@ +{ + "extends": ".eslintrc.json", + "rules": { + "no-sync": "warn", + "strict": ["warn", "never"] + } +} diff --git a/.github/feature-requests.yml b/.github/feature-requests.yml new file mode 100644 index 0000000000..18055b8448 --- /dev/null +++ b/.github/feature-requests.yml @@ -0,0 +1,34 @@ +{ + typeLabel: { + name: 'feature-request' + }, + candidateMilestone: { + number: 107, + name: 'Backlog Candidates' + }, + approvedMilestone: { + number: 8, + name: 'Backlog' + }, + onLabeled: { + delay: 60, + perform: true + }, + onCandidateMilestoned: { + candidatesComment: "This feature request is now a candidate for our backlog. The community has 60 days to upvote the issue. If it receives 20 upvotes we will move it to our backlog. If not, we will close it. To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/vscode-issue-lifecycle).\n\nHappy Coding!", + perform: true + }, + onMonitorUpvotes: { + upvoteThreshold: 20, + acceptanceComment: ":slightly_smiling_face: This feature request received a sufficient number of community upvotes and we moved it to our backlog. To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/vscode-issue-lifecycle).\n\nHappy Coding!", + perform: true + }, + onMonitorDaysOnCandidateMilestone: { + daysOnMilestone: 60, + warningPeriod: 10, + numberOfCommentsToPreventAutomaticRejection: 20, + rejectionComment: ":slightly_frowning_face: In the last 60 days, this feature request has received less than 20 community upvotes and we closed it. Still a big Thank You to you for taking the time to create this issue! To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/vscode-issue-lifecycle).\n\nHappy Coding!", + warningComment: "This feature request has not yet received the 20 community upvotes it takes to make to our backlog. 10 days to go. To learn more about how we handle feature requests, please see our [documentation](https://aka.ms/vscode-issue-lifecycle).\n\nHappy Coding", + perform: true + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 91c12891d8..f8a7796c85 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,14 +35,14 @@ jobs: name: Install Dependencies - run: yarn electron x64 name: Download Electron - - run: yarn gulp hygiene --skip-tslint + - run: yarn gulp hygiene name: Run Hygiene Checks - - run: yarn gulp tslint - name: Run TSLint Checks - run: yarn strict-null-check # {{SQL CARBON EDIT}} add step name: Run Strict Null Check # - run: yarn monaco-compile-check {{SQL CARBON EDIT}} remove step # name: Run Monaco Editor Checks + - run: yarn valid-layers-check + name: Run Valid Layers Checks - run: yarn compile name: Compile Sources # - run: yarn download-builtin-extensions {{SQL CARBON EDIT}} remove step @@ -69,14 +69,14 @@ jobs: name: Install Dependencies - run: yarn electron name: Download Electron - - run: yarn gulp hygiene --skip-tslint + - run: yarn gulp hygiene name: Run Hygiene Checks - - run: yarn gulp tslint - name: Run TSLint Checks - run: yarn strict-null-check # {{SQL CARBON EDIT}} add step name: Run Strict Null Check # - run: yarn monaco-compile-check {{SQL CARBON EDIT}} remove step # name: Run Monaco Editor Checks + - run: yarn valid-layers-check + name: Run Valid Layers Checks - run: yarn compile name: Compile Sources # - run: yarn download-builtin-extensions {{SQL CARBON EDIT}} remove step @@ -100,14 +100,14 @@ jobs: name: Install Dependencies - run: yarn electron x64 name: Download Electron - - run: yarn gulp hygiene --skip-tslint + - run: yarn gulp hygiene name: Run Hygiene Checks - - run: yarn gulp tslint - name: Run TSLint Checks - run: yarn strict-null-check # {{SQL CARBON EDIT}} add step name: Run Strict Null Check # - run: yarn monaco-compile-check {{SQL CARBON EDIT}} remove step # name: Run Monaco Editor Checks + - run: yarn valid-layers-check + name: Run Valid Layers Checks - run: yarn compile name: Compile Sources # - run: yarn download-builtin-extensions {{SQL CARBON EDIT}} remove step diff --git a/.github/workflows/tslint.yml b/.github/workflows/tslint.yml deleted file mode 100644 index d76396baf2..0000000000 --- a/.github/workflows/tslint.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: TSLint Enforcement -on: [pull_request] -jobs: - job: - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - uses: actions/checkout@v1 - - name: TSLint - uses: aaomidi/gh-action-tslint@master - with: - token: ${{ secrets.GITHUB_TOKEN }} - tslint_config: 'tslint-sql.json' diff --git a/.vscode/extensions.json b/.vscode/extensions.json index b4336e7d12..2cd0a32b9c 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -2,7 +2,6 @@ // See https://go.microsoft.com/fwlink/?LinkId=827846 // for the documentation about the extensions.json format "recommendations": [ - "ms-vscode.vscode-typescript-tslint-plugin", "dbaeumer.vscode-eslint", "EditorConfig.EditorConfig", "msjsdiag.debugger-for-chrome" diff --git a/.vscode/launch.json b/.vscode/launch.json index 224f940153..b0c78892d1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,10 +20,7 @@ "restart": true, "outFiles": [ "${workspaceFolder}/out/**/*.js" - ], - "presentation": { - "hidden": true, - } + ] }, { "type": "chrome", @@ -69,10 +66,7 @@ "type": "chrome", "request": "attach", "name": "Attach to azuredatastudio", - "port": 9222, - "presentation": { - "hidden": true - } + "port": 9222 }, { "type": "chrome", @@ -96,12 +90,12 @@ "urlFilter": "*workbench.html*", "runtimeArgs": [ "--inspect=5875", - "--no-cached-data" + "--no-cached-data", ], "webRoot": "${workspaceFolder}", - "presentation": { - "hidden": true - } + // Settings for js-debug: + "pauseForSourceMap": false, + "outFiles": ["${workspaceFolder}/out/**/*.js"], }, { "type": "node", diff --git a/.vscode/searches/ts36031.code-search b/.vscode/searches/ts36031.code-search new file mode 100644 index 0000000000..fb6cf8a431 --- /dev/null +++ b/.vscode/searches/ts36031.code-search @@ -0,0 +1,10 @@ +# Query: \\w+\\?\\..+![(.[] +# Flags: RegExp +# ContextLines: 2 + +src/vs/base/browser/ui/tree/asyncDataTree.ts: + 270 } : undefined, + 271 isChecked: options.ariaProvider!.isChecked ? (e) => { + 272: return options.ariaProvider?.isChecked!(e.element as T); + 273 } : undefined + 274 }, diff --git a/.vscode/settings.json b/.vscode/settings.json index ef1f7370d2..e0fd4e2342 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -37,6 +37,11 @@ } } ], + "eslint.options": { + "rulePaths": [ + "./build/lib/eslint" + ] + }, "typescript.tsdk": "node_modules/typescript/lib", "npm.exclude": "**/extensions/**", "npm.packageManager": "yarn", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 042f5058f8..2c9774d518 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -59,14 +59,6 @@ "applyTo": "allDocuments" } }, - { - "type": "gulp", - "task": "tslint", - "label": "Run tslint", - "problemMatcher": [ - "$tslint5" - ] - }, { "label": "Run tests", "type": "shell", @@ -104,7 +96,6 @@ "command": "yarn web -- --no-launch", "label": "Run web", "isBackground": true, - // This section to make error go away when launching the debug config "problemMatcher": { "pattern": { "regexp": "" @@ -118,5 +109,13 @@ "reveal": "never" } }, + { + "type": "npm", + "script": "eslint", + "problemMatcher": { + "source": "eslint", + "base": "$eslint-stylish" + } + } ] } diff --git a/.yarnrc b/.yarnrc index 85baaa63a7..2c769cfba1 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,3 +1,3 @@ disturl "https://atom.io/download/electron" -target "6.1.6" +target "7.1.7" runtime "electron" diff --git a/build/azure-pipelines/darwin/continuous-build-darwin.yml b/build/azure-pipelines/darwin/continuous-build-darwin.yml index 57a727f50a..0ef09deaab 100644 --- a/build/azure-pipelines/darwin/continuous-build-darwin.yml +++ b/build/azure-pipelines/darwin/continuous-build-darwin.yml @@ -24,20 +24,17 @@ steps: yarn electron x64 displayName: Download Electron - script: | - yarn gulp hygiene --skip-tslint + yarn gulp hygiene displayName: Run Hygiene Checks -- script: | - yarn gulp tslint - displayName: Run TSLint Checks - script: | # {{SQL CARBON EDIT}} add step yarn strict-null-check displayName: Run Strict Null Check. -- script: | # {{SQL CARBON EDIT}} add step - yarn tslint - displayName: Run TSLint (gci) # - script: | {{SQL CARBON EDIT}} remove step # yarn monaco-compile-check # displayName: Run Monaco Editor Checks +- script: | + yarn valid-layers-check + displayName: Run Valid Layers Checks - script: | yarn compile displayName: Compile Sources diff --git a/build/azure-pipelines/linux/continuous-build-linux.yml b/build/azure-pipelines/linux/continuous-build-linux.yml index 4cb71700e0..b62a8d768c 100644 --- a/build/azure-pipelines/linux/continuous-build-linux.yml +++ b/build/azure-pipelines/linux/continuous-build-linux.yml @@ -32,20 +32,17 @@ steps: yarn electron x64 displayName: Download Electron - script: | - yarn gulp hygiene --skip-tslint + yarn gulp hygiene displayName: Run Hygiene Checks -- script: | - yarn gulp tslint - displayName: Run TSLint Checks -- script: | # {{SQL CARBON EDIT}} add gci checks - yarn tslint - displayName: Run TSLint (gci) - script: | # {{SQL CARBON EDIT}} add strict null check yarn strict-null-check displayName: Run Strict Null Check # - script: | {{SQL CARBON EDIT}} remove monaco editor checks # yarn monaco-compile-check # displayName: Run Monaco Editor Checks +- script: | + yarn valid-layers-check + displayName: Run Valid Layers Checks - script: | yarn compile displayName: Compile Sources diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index 8029f8a566..db6524be03 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -88,10 +88,10 @@ steps: - script: | set -e - yarn gulp hygiene --skip-tslint - yarn gulp tslint + yarn gulp hygiene yarn monaco-compile-check - displayName: Run hygiene, tslint and monaco compile checks + yarn valid-layers-check + displayName: Run hygiene, monaco compile & valid layers checks condition: and(succeeded(), ne(variables['CacheExists-Compilation'], 'true'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - script: | diff --git a/build/azure-pipelines/sql-product-compile.yml b/build/azure-pipelines/sql-product-compile.yml index 52fa5bcbc1..66f004f68f 100644 --- a/build/azure-pipelines/sql-product-compile.yml +++ b/build/azure-pipelines/sql-product-compile.yml @@ -87,8 +87,9 @@ steps: - script: | set -e - yarn gulp hygiene --skip-tslint - yarn gulp tslint + yarn gulp hygiene + yarn strict-null-check + yarn valid-layers-check displayName: Run hygiene, tslint condition: and(succeeded(), ne(variables['CacheRestored-Compilation'], 'true'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) diff --git a/build/azure-pipelines/win32/continuous-build-win32.yml b/build/azure-pipelines/win32/continuous-build-win32.yml index 6df36f238b..ecb243ee56 100644 --- a/build/azure-pipelines/win32/continuous-build-win32.yml +++ b/build/azure-pipelines/win32/continuous-build-win32.yml @@ -29,20 +29,17 @@ steps: - powershell: | yarn electron - script: | - yarn gulp hygiene --skip-tslint + yarn gulp hygiene displayName: Run Hygiene Checks -- script: | - yarn gulp tslint - displayName: Run TSLint Checks -- script: | # {{SQL CARBON EDIT}} add step - yarn tslint - displayName: Run TSLint (gci) - script: | # {{SQL CARBON EDIT}} add step yarn strict-null-check displayName: Run Strict Null Check # - powershell: | {{SQL CARBON EDIT}} remove step # yarn monaco-compile-check # displayName: Run Monaco Editor Checks +- script: | + yarn valid-layers-check + displayName: Run Valid Layers Checks - powershell: | yarn compile displayName: Compile Sources diff --git a/build/gulpfile.editor.js b/build/gulpfile.editor.js index 83d3dc6c85..993db90999 100644 --- a/build/gulpfile.editor.js +++ b/build/gulpfile.editor.js @@ -17,14 +17,14 @@ const compilation = require('./lib/compilation'); const monacoapi = require('./monaco/api'); const fs = require('fs'); -var root = path.dirname(__dirname); -var sha1 = util.getVersion(root); -var semver = require('./monaco/package.json').version; -var headerVersion = semver + '(' + sha1 + ')'; +let root = path.dirname(__dirname); +let sha1 = util.getVersion(root); +let semver = require('./monaco/package.json').version; +let headerVersion = semver + '(' + sha1 + ')'; // Build -var editorEntryPoints = [ +let editorEntryPoints = [ { name: 'vs/editor/editor.main', include: [], @@ -40,11 +40,11 @@ var editorEntryPoints = [ } ]; -var editorResources = [ +let editorResources = [ 'out-editor-build/vs/base/browser/ui/codiconLabel/**/*.ttf' ]; -var BUNDLED_FILE_HEADER = [ +let BUNDLED_FILE_HEADER = [ '/*!-----------------------------------------------------------', ' * Copyright (c) Microsoft Corporation. All rights reserved.', ' * Version: ' + headerVersion, @@ -197,7 +197,7 @@ const compileEditorESMTask = task.define('compile-editor-esm', () => { }); function toExternalDTS(contents) { - let lines = contents.split('\n'); + let lines = contents.split(/\r\n|\r|\n/); let killNextCloseCurlyBrace = false; for (let i = 0; i < lines.length; i++) { let line = lines[i]; @@ -263,7 +263,7 @@ const finalEditorResourcesTask = task.define('final-editor-resources', () => { // package.json gulp.src('build/monaco/package.json') .pipe(es.through(function (data) { - var json = JSON.parse(data.contents.toString()); + let json = JSON.parse(data.contents.toString()); json.private = false; data.contents = Buffer.from(JSON.stringify(json, null, ' ')); this.emit('data', data); @@ -307,10 +307,10 @@ const finalEditorResourcesTask = task.define('final-editor-resources', () => { return; } - var relativePathToMap = path.relative(path.join(data.relative), path.join('min-maps', data.relative + '.map')); + let relativePathToMap = path.relative(path.join(data.relative), path.join('min-maps', data.relative + '.map')); - var strContents = data.contents.toString(); - var newStr = '//# sourceMappingURL=' + relativePathToMap.replace(/\\/g, '/'); + let strContents = data.contents.toString(); + let newStr = '//# sourceMappingURL=' + relativePathToMap.replace(/\\/g, '/'); strContents = strContents.replace(/\/\/# sourceMappingURL=[^ ]+$/, newStr); data.contents = Buffer.from(strContents); @@ -353,6 +353,13 @@ gulp.task('editor-distro', ) ); +gulp.task('monacodts', task.define('monacodts', () => { + const result = monacoapi.execute(); + fs.writeFileSync(result.filePath, result.content); + fs.writeFileSync(path.join(root, 'src/vs/editor/common/standalone/standaloneEnums.ts'), result.enums); + return Promise.resolve(true); +})); + //#region monaco type checking function createTscCompileTask(watch) { diff --git a/build/gulpfile.hygiene.js b/build/gulpfile.hygiene.js index 9abf539025..4d36ca661c 100644 --- a/build/gulpfile.hygiene.js +++ b/build/gulpfile.hygiene.js @@ -8,10 +8,8 @@ const gulp = require('gulp'); const filter = require('gulp-filter'); const es = require('event-stream'); -const gulptslint = require('gulp-tslint'); const gulpeslint = require('gulp-eslint'); const tsfmt = require('typescript-formatter'); -const tslint = require('tslint'); const VinylFile = require('vinyl'); const vfs = require('vinyl-fs'); const path = require('path'); @@ -87,7 +85,7 @@ const indentationFilter = [ '!src/typings/**/*.d.ts', '!extensions/**/*.d.ts', '!**/*.{svg,exe,png,bmp,scpt,bat,cmd,cur,ttf,woff,eot,md,ps1,template,yaml,yml,d.ts.recipe,ico,icns}', - '!build/{lib,tslintRules,download}/**/*.js', + '!build/{lib,download}/**/*.js', '!build/**/*.sh', '!build/azure-pipelines/**/*.js', '!build/azure-pipelines/**/*.config', @@ -173,7 +171,7 @@ const copyrightFilter = [ '!**/*.bacpac' ]; -const eslintFilter = [ +const jsHygieneFilter = [ 'src/**/*.js', 'build/gulpfile.*.js', '!src/vs/loader.js', @@ -186,7 +184,10 @@ const eslintFilter = [ '!**/test/**' ]; -const tslintBaseFilter = [ +const tsHygieneFilter = [ + 'src/**/*.ts', + 'test/**/*.ts', + 'extensions/**/*.ts', '!**/fixtures/**', '!**/typings/**', '!**/node_modules/**', @@ -197,68 +198,17 @@ const tslintBaseFilter = [ '!extensions/html-language-features/server/lib/jquery.d.ts', '!extensions/big-data-cluster/src/bigDataCluster/controller/apiGenerated.ts', // {{SQL CARBON EDIT}}, '!extensions/big-data-cluster/src/bigDataCluster/controller/tokenApiGenerated.ts', // {{SQL CARBON EDIT}}, - '!src/vs/workbench/services/themes/common/textMateScopeMatcher.ts' // {{SQL CARBON EDIT}} skip this because we have no plans on touching this and its not ours + '!src/vs/workbench/services/themes/common/textMateScopeMatcher.ts', // {{SQL CARBON EDIT}} skip this because we have no plans on touching this and its not ours + '!src/vs/workbench/contrib/extensions/browser/extensionTipsService.ts' // {{SQL CARBON EDIT}} skip this because known issue ]; -// {{SQL CARBON EDIT}} -const sqlFilter = [ - 'src/sql/**', - 'extensions/**', - // Ignore VS Code extensions - '!extensions/bat/**', - '!extensions/configuration-editing/**', - '!extensions/docker/**', - '!extensions/extension-editing/**', - '!extensions/git/**', - '!extensions/git-ui/**', - '!extensions/image-preview/**', - '!extensions/insights-default/**', - '!extensions/json/**', - '!extensions/json-language-features/**', - '!extensions/markdown-basics/**', - '!extensions/markdown-language-features/**', - '!extensions/merge-conflict/**', - '!extensions/powershell/**', - '!extensions/python/**', - '!extensions/r/**', - '!extensions/theme-*/**', - '!extensions/vscode-*/**', - '!extensions/xml/**', - '!extensions/xml-language-features/**', - '!extensions/yarml/**', -]; - -const tslintCoreFilter = [ - 'src/**/*.ts', - 'test/**/*.ts', - '!extensions/**/*.ts', - '!test/automation/**', - '!test/smoke/**', - ...tslintBaseFilter -]; - -const tslintExtensionsFilter = [ +const sqlHygieneFilter = [ // for rules we want to only apply to our code + 'src/sql/**/*.ts', + '!**/node_modules/**', 'extensions/**/*.ts', - '!src/**/*.ts', - '!test/**/*.ts', - 'test/automation/**/*.ts', - ...tslintBaseFilter + '!extensions/{git,search-result,vscode-test-resolver,extension-editing,json-language-features,vscode-colorize-tests}/**/*.ts', ]; -const tslintHygieneFilter = [ - 'src/**/*.ts', - 'test/**/*.ts', - 'extensions/**/*.ts', - '!src/vs/workbench/contrib/extensions/browser/extensionTipsService.ts', // {{SQL CARBON EDIT}} known formatting issue do to commenting out code - ...tslintBaseFilter -]; - -const fileLengthFilter = filter([ - '**', - '!extensions/import/*.docx', - '!extensions/admin-tool-ext-win/license/**' -], {restore: true}); - const copyrightHeaderLines = [ '/*---------------------------------------------------------------------------------------------', ' * Copyright (c) Microsoft Corporation. All rights reserved.', @@ -268,27 +218,17 @@ const copyrightHeaderLines = [ gulp.task('eslint', () => { return vfs.src(all, { base: '.', follow: true, allowEmpty: true }) - .pipe(filter(eslintFilter)) - .pipe(gulpeslint('src/.eslintrc')) + .pipe(filter(jsHygieneFilter.concat(tsHygieneFilter))) + .pipe(gulpeslint({ + configFile: '.eslintrc.json', + rulePaths: ['./build/lib/eslint'] + })) .pipe(gulpeslint.formatEach('compact')) - .pipe(gulpeslint.failAfterError()); -}); - -gulp.task('tslint', () => { - return es.merge([ - - // Core: include type information (required by certain rules like no-nodejs-globals) - vfs.src(all, { base: '.', follow: true, allowEmpty: true }) - .pipe(filter(tslintCoreFilter)) - .pipe(gulptslint.default({ rulesDirectory: 'build/lib/tslint', program: tslint.Linter.createProgram('src/tsconfig.json') })) - .pipe(gulptslint.default.report({ emitError: true })), - - // Exenstions: do not include type information - vfs.src(all, { base: '.', follow: true, allowEmpty: true }) - .pipe(filter(tslintExtensionsFilter)) - .pipe(gulptslint.default({ rulesDirectory: 'build/lib/tslint' })) - .pipe(gulptslint.default.report({ emitError: true })) - ]).pipe(es.through()); + .pipe(gulpeslint.results(results => { + if (results.warningCount > 0 || results.errorCount > 0) { + throw new Error('eslint failed with warnings and/or errors'); + } + })); }); function checkPackageJSON(actualPath) { @@ -310,7 +250,7 @@ function checkPackageJSON(actualPath) { const checkPackageJSONTask = task.define('check-package-json', () => { return gulp.src('package.json') - .pipe(es.through(function() { + .pipe(es.through(function () { checkPackageJSON.call(this, 'remote/package.json'); checkPackageJSON.call(this, 'remote/web/package.json'); })); @@ -377,8 +317,6 @@ function hygiene(some) { replace: undefined, tsconfig: undefined, tsconfigFile: undefined, - tslint: undefined, - tslintFile: undefined, tsfmtFile: undefined, vscode: undefined, vscodeFile: undefined @@ -387,7 +325,7 @@ function hygiene(some) { let formatted = result.dest.replace(/\r\n/gm, '\n'); if (original !== formatted) { - console.error("File not formatted. Run the 'Format Document' command to fix it:", file.relative); + console.error('File not formatted. Run the \'Format Document\' command to fix it:', file.relative); errorCount++; } cb(null, file); @@ -397,33 +335,6 @@ function hygiene(some) { }); }); - const filelength = es.through(function (file) { - - const fileName = path.basename(file.relative); - const fileDir = path.dirname(file.relative); - //check the filename is < 50 characters (basename gets the filename with extension). - if (fileName.length > 50) { - console.error(`File name '${fileName}' under ${fileDir} is too long. Rename file to have less than 50 characters.`); - errorCount++; - } - if (file.relative.length > 150) { - console.error(`File path ${file.relative} exceeds acceptable file-length. Rename the path to have less than 150 characters.`); - errorCount++; - } - - this.emit('data', file); - }); - - const tslintConfiguration = tslint.Configuration.findConfiguration('tslint.json', '.'); - const tslintOptions = { fix: false, formatter: 'json' }; - const tsLinter = new tslint.Linter(tslintOptions); - - const tsl = es.through(function (file) { - const contents = file.contents.toString('utf8'); - tsLinter.lint(file.relative, contents, tslintConfiguration.results); - this.emit('data', file); - }); - let input; if (Array.isArray(some) || typeof some === 'string' || !some) { @@ -437,22 +348,9 @@ function hygiene(some) { input = some; } - // {{SQL CARBON EDIT}} Linting for SQL - const tslintSqlConfiguration = tslint.Configuration.findConfiguration('tslint-sql.json', '.'); - const tslintSqlOptions = { fix: false, formatter: 'json' }; - const sqlTsLinter = new tslint.Linter(tslintSqlOptions); - - const sqlTsl = es.through(function (file) { //TODO restore - const contents = file.contents.toString('utf8'); - sqlTsLinter.lint(file.relative, contents, tslintSqlConfiguration.results); - }); - const productJsonFilter = filter('product.json', { restore: true }); const result = input - .pipe(fileLengthFilter) - .pipe(filelength) - .pipe(fileLengthFilter.restore) .pipe(filter(f => !f.stat.isDirectory())) .pipe(productJsonFilter) .pipe(process.env['BUILD_SOURCEVERSION'] ? es.through() : productJson) @@ -462,25 +360,36 @@ function hygiene(some) { .pipe(filter(copyrightFilter)) .pipe(copyrights); - let typescript = result - .pipe(filter(tslintHygieneFilter)) + const typescript = result + .pipe(filter(tsHygieneFilter)) .pipe(formatting); - if (!process.argv.some(arg => arg === '--skip-tslint')) { - typescript = typescript.pipe(tsl); - typescript = typescript - .pipe(filter(sqlFilter)) // {{SQL CARBON EDIT}} - .pipe(sqlTsl); - } - const javascript = result - .pipe(filter(eslintFilter)) - .pipe(gulpeslint('src/.eslintrc')) + .pipe(filter(jsHygieneFilter.concat(tsHygieneFilter))) + .pipe(gulpeslint({ + configFile: '.eslintrc.json', + rulePaths: ['./build/lib/eslint'] + })) .pipe(gulpeslint.formatEach('compact')) - .pipe(gulpeslint.failAfterError()); + .pipe(gulpeslint.results(results => { + errorCount += results.warningCount; + errorCount += results.errorCount; + })); + + const sqlJavascript = result + .pipe(filter(sqlHygieneFilter)) + .pipe(gulpeslint({ + configFile: '.eslintrc.sql.json', + rulePaths: ['./build/lib/eslint'] + })) + .pipe(gulpeslint.formatEach('compact')) + .pipe(gulpeslint.results(results => { + errorCount += results.warningCount; + errorCount += results.errorCount; + })); let count = 0; - return es.merge(typescript, javascript) + return es.merge(typescript, javascript, sqlJavascript) .pipe(es.through(function (data) { count++; if (process.env['TRAVIS'] && count % 10 === 0) { @@ -489,33 +398,6 @@ function hygiene(some) { this.emit('data', data); }, function () { process.stdout.write('\n'); - - const tslintResult = tsLinter.getResult(); - if (tslintResult.failures.length > 0) { - for (const failure of tslintResult.failures) { - const name = failure.getFileName(); - const position = failure.getStartPosition(); - const line = position.getLineAndCharacter().line; - const character = position.getLineAndCharacter().character; - - console.error(`${name}:${line + 1}:${character + 1}:${failure.getFailure()}`); - } - errorCount += tslintResult.failures.length; - } - - const sqlTslintResult = sqlTsLinter.getResult(); - if (sqlTslintResult.failures.length > 0) { - for (const failure of sqlTslintResult.failures) { - const name = failure.getFileName(); - const position = failure.getStartPosition(); - const line = position.getLineAndCharacter().line; - const character = position.getLineAndCharacter().character; - - console.error(`${name}:${line + 1}:${character + 1}:${failure.getFailure()}`); - } - errorCount += sqlTslintResult.failures.length; - } - if (errorCount > 0) { this.emit('error', 'Hygiene failed with ' + errorCount + ' errors. Check \'build/gulpfile.hygiene.js\'.'); } else { diff --git a/build/gulpfile.reh.js b/build/gulpfile.reh.js index 80309a2bc8..2679341f73 100644 --- a/build/gulpfile.reh.js +++ b/build/gulpfile.reh.js @@ -118,7 +118,7 @@ function mixinServer(watch) { const packageJSONPath = path.join(path.dirname(__dirname), 'package.json'); function exec(cmdLine) { console.log(cmdLine); - cp.execSync(cmdLine, { stdio: "inherit" }); + cp.execSync(cmdLine, { stdio: 'inherit' }); } function checkout() { const packageJSON = JSON.parse(fs.readFileSync(packageJSONPath).toString()); diff --git a/build/gulpfile.sql.js b/build/gulpfile.sql.js index e814058526..ce420871d0 100644 --- a/build/gulpfile.sql.js +++ b/build/gulpfile.sql.js @@ -102,7 +102,7 @@ function installService() { let runtime = p.runtimeId; // fix path since it won't be correct config.installDirectory = path.join(__dirname, '../extensions/mssql/src', config.installDirectory); - var installer = new serviceDownloader(config); + let installer = new serviceDownloader(config); let serviceInstallFolder = installer.getInstallDirectory(runtime); console.log('Cleaning up the install folder: ' + serviceInstallFolder); return del(serviceInstallFolder + '/*').then(() => { @@ -123,7 +123,7 @@ gulp.task('install-ssmsmin', () => { const runtime = 'Windows_64'; // admin-tool-ext is a windows only extension, and we only ship a 64 bit version, so locking the binaries as such // fix path since it won't be correct config.installDirectory = path.join(__dirname, '..', 'extensions', 'admin-tool-ext-win', config.installDirectory); - var installer = new serviceDownloader(config); + let installer = new serviceDownloader(config); const serviceInstallFolder = installer.getInstallDirectory(runtime); const serviceCleanupFolder = path.join(serviceInstallFolder, '..'); console.log('Cleaning up the install folder: ' + serviceCleanupFolder); diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index a797772ad9..43daac0b49 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -150,9 +150,9 @@ gulp.task(minifyVSCodeTask); * @return {Object} A map of paths to checksums. */ function computeChecksums(out, filenames) { - var result = {}; + let result = {}; filenames.forEach(function (filename) { - var fullPath = path.join(process.cwd(), out, filename); + let fullPath = path.join(process.cwd(), out, filename); result[filename] = computeChecksum(fullPath); }); return result; @@ -165,9 +165,9 @@ function computeChecksums(out, filenames) { * @return {string} The checksum for `filename`. */ function computeChecksum(filename) { - var contents = fs.readFileSync(filename); + let contents = fs.readFileSync(filename); - var hash = crypto + let hash = crypto .createHash('md5') .update(contents) .digest('base64') diff --git a/build/gulpfile.vscode.linux.js b/build/gulpfile.vscode.linux.js index 76fb8bc354..1e3f338da3 100644 --- a/build/gulpfile.vscode.linux.js +++ b/build/gulpfile.vscode.linux.js @@ -23,7 +23,7 @@ const commit = util.getVersion(root); const linuxPackageRevision = Math.floor(new Date().getTime() / 1000); function getDebPackageArch(arch) { - return { x64: 'amd64', arm: 'armhf', arm64: "arm64" }[arch]; + return { x64: 'amd64', arm: 'armhf', arm64: 'arm64' }[arch]; } function prepareDebPackage(arch) { @@ -118,7 +118,7 @@ function getRpmBuildPath(rpmArch) { } function getRpmPackageArch(arch) { - return { x64: 'x86_64', arm: 'armhf', arm64: "arm64" }[arch]; + return { x64: 'x86_64', arm: 'armhf', arm64: 'arm64' }[arch]; } function prepareRpmPackage(arch) { diff --git a/build/lib/eslint/code-import-patterns.js b/build/lib/eslint/code-import-patterns.js new file mode 100644 index 0000000000..5babda400c --- /dev/null +++ b/build/lib/eslint/code-import-patterns.js @@ -0,0 +1,59 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +const path_1 = require("path"); +const minimatch = require("minimatch"); +const utils_1 = require("./utils"); +module.exports = new class { + constructor() { + this.meta = { + messages: { + badImport: 'Imports violates \'{{restrictions}}\' restrictions. See https://github.com/microsoft/vscode/wiki/Source-Code-Organization' + }, + docs: { + url: 'https://github.com/microsoft/vscode/wiki/Source-Code-Organization' + } + }; + } + create(context) { + const configs = context.options; + for (const config of configs) { + if (minimatch(context.getFilename(), config.target)) { + return utils_1.createImportRuleListener((node, value) => this._checkImport(context, config, node, value)); + } + } + return {}; + } + _checkImport(context, config, node, path) { + // resolve relative paths + if (path[0] === '.') { + path = path_1.join(context.getFilename(), path); + } + let restrictions; + if (typeof config.restrictions === 'string') { + restrictions = [config.restrictions]; + } + else { + restrictions = config.restrictions; + } + let matched = false; + for (const pattern of restrictions) { + if (minimatch(path, pattern)) { + matched = true; + break; + } + } + if (!matched) { + // None of the restrictions matched + context.report({ + loc: node.loc, + messageId: 'badImport', + data: { + restrictions: restrictions.join(' or ') + } + }); + } + } +}; diff --git a/build/lib/eslint/code-import-patterns.ts b/build/lib/eslint/code-import-patterns.ts new file mode 100644 index 0000000000..e2b427abe2 --- /dev/null +++ b/build/lib/eslint/code-import-patterns.ts @@ -0,0 +1,75 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; +import { TSESTree } from '@typescript-eslint/experimental-utils'; +import { join } from 'path'; +import * as minimatch from 'minimatch'; +import { createImportRuleListener } from './utils'; + +interface ImportPatternsConfig { + target: string; + restrictions: string | string[]; +} + +export = new class implements eslint.Rule.RuleModule { + + readonly meta: eslint.Rule.RuleMetaData = { + messages: { + badImport: 'Imports violates \'{{restrictions}}\' restrictions. See https://github.com/microsoft/vscode/wiki/Source-Code-Organization' + }, + docs: { + url: 'https://github.com/microsoft/vscode/wiki/Source-Code-Organization' + } + }; + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + + const configs = context.options; + + for (const config of configs) { + if (minimatch(context.getFilename(), config.target)) { + return createImportRuleListener((node, value) => this._checkImport(context, config, node, value)); + } + } + + return {}; + } + + private _checkImport(context: eslint.Rule.RuleContext, config: ImportPatternsConfig, node: TSESTree.Node, path: string) { + + // resolve relative paths + if (path[0] === '.') { + path = join(context.getFilename(), path); + } + + let restrictions: string[]; + if (typeof config.restrictions === 'string') { + restrictions = [config.restrictions]; + } else { + restrictions = config.restrictions; + } + + let matched = false; + for (const pattern of restrictions) { + if (minimatch(path, pattern)) { + matched = true; + break; + } + } + + if (!matched) { + // None of the restrictions matched + context.report({ + loc: node.loc, + messageId: 'badImport', + data: { + restrictions: restrictions.join(' or ') + } + }); + } + } +}; + diff --git a/build/lib/eslint/code-layering.js b/build/lib/eslint/code-layering.js new file mode 100644 index 0000000000..bac676755b --- /dev/null +++ b/build/lib/eslint/code-layering.js @@ -0,0 +1,68 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +const path_1 = require("path"); +const utils_1 = require("./utils"); +module.exports = new class { + constructor() { + this.meta = { + messages: { + layerbreaker: 'Bad layering. You are not allowed to access {{from}} from here, allowed layers are: [{{allowed}}]' + }, + docs: { + url: 'https://github.com/microsoft/vscode/wiki/Source-Code-Organization' + } + }; + } + create(context) { + const fileDirname = path_1.dirname(context.getFilename()); + const parts = fileDirname.split(/\\|\//); + const ruleArgs = context.options[0]; + let config; + for (let i = parts.length - 1; i >= 0; i--) { + if (ruleArgs[parts[i]]) { + config = { + allowed: new Set(ruleArgs[parts[i]]).add(parts[i]), + disallowed: new Set() + }; + Object.keys(ruleArgs).forEach(key => { + if (!config.allowed.has(key)) { + config.disallowed.add(key); + } + }); + break; + } + } + if (!config) { + // nothing + return {}; + } + return utils_1.createImportRuleListener((node, path) => { + if (path[0] === '.') { + path = path_1.join(path_1.dirname(context.getFilename()), path); + } + const parts = path_1.dirname(path).split(/\\|\//); + for (let i = parts.length - 1; i >= 0; i--) { + const part = parts[i]; + if (config.allowed.has(part)) { + // GOOD - same layer + break; + } + if (config.disallowed.has(part)) { + // BAD - wrong layer + context.report({ + loc: node.loc, + messageId: 'layerbreaker', + data: { + from: part, + allowed: [...config.allowed.keys()].join(', ') + } + }); + break; + } + } + }); + } +}; diff --git a/build/lib/eslint/code-layering.ts b/build/lib/eslint/code-layering.ts new file mode 100644 index 0000000000..8ff59ad87a --- /dev/null +++ b/build/lib/eslint/code-layering.ts @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; +import { join, dirname } from 'path'; +import { createImportRuleListener } from './utils'; + +type Config = { + allowed: Set; + disallowed: Set; +}; + +export = new class implements eslint.Rule.RuleModule { + + readonly meta: eslint.Rule.RuleMetaData = { + messages: { + layerbreaker: 'Bad layering. You are not allowed to access {{from}} from here, allowed layers are: [{{allowed}}]' + }, + docs: { + url: 'https://github.com/microsoft/vscode/wiki/Source-Code-Organization' + } + }; + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + + const fileDirname = dirname(context.getFilename()); + const parts = fileDirname.split(/\\|\//); + const ruleArgs = >context.options[0]; + + let config: Config | undefined; + for (let i = parts.length - 1; i >= 0; i--) { + if (ruleArgs[parts[i]]) { + config = { + allowed: new Set(ruleArgs[parts[i]]).add(parts[i]), + disallowed: new Set() + }; + Object.keys(ruleArgs).forEach(key => { + if (!config!.allowed.has(key)) { + config!.disallowed.add(key); + } + }); + break; + } + } + + if (!config) { + // nothing + return {}; + } + + return createImportRuleListener((node, path) => { + if (path[0] === '.') { + path = join(dirname(context.getFilename()), path); + } + + const parts = dirname(path).split(/\\|\//); + for (let i = parts.length - 1; i >= 0; i--) { + const part = parts[i]; + + if (config!.allowed.has(part)) { + // GOOD - same layer + break; + } + + if (config!.disallowed.has(part)) { + // BAD - wrong layer + context.report({ + loc: node.loc, + messageId: 'layerbreaker', + data: { + from: part, + allowed: [...config!.allowed.keys()].join(', ') + } + }); + break; + } + } + }); + } +}; + diff --git a/build/lib/eslint/code-no-nls-in-standalone-editor.js b/build/lib/eslint/code-no-nls-in-standalone-editor.js new file mode 100644 index 0000000000..1f1eabfcba --- /dev/null +++ b/build/lib/eslint/code-no-nls-in-standalone-editor.js @@ -0,0 +1,38 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +const path_1 = require("path"); +const utils_1 = require("./utils"); +module.exports = new class NoNlsInStandaloneEditorRule { + constructor() { + this.meta = { + messages: { + noNls: 'Not allowed to import vs/nls in standalone editor modules. Use standaloneStrings.ts' + } + }; + } + create(context) { + const fileName = context.getFilename(); + if (/vs(\/|\\)editor(\/|\\)standalone(\/|\\)/.test(fileName) + || /vs(\/|\\)editor(\/|\\)common(\/|\\)standalone(\/|\\)/.test(fileName) + || /vs(\/|\\)editor(\/|\\)editor.api/.test(fileName) + || /vs(\/|\\)editor(\/|\\)editor.main/.test(fileName) + || /vs(\/|\\)editor(\/|\\)editor.worker/.test(fileName)) { + return utils_1.createImportRuleListener((node, path) => { + // resolve relative paths + if (path[0] === '.') { + path = path_1.join(context.getFilename(), path); + } + if (/vs(\/|\\)nls/.test(path)) { + context.report({ + loc: node.loc, + messageId: 'noNls' + }); + } + }); + } + return {}; + } +}; diff --git a/build/lib/eslint/code-no-nls-in-standalone-editor.ts b/build/lib/eslint/code-no-nls-in-standalone-editor.ts new file mode 100644 index 0000000000..8f1477250c --- /dev/null +++ b/build/lib/eslint/code-no-nls-in-standalone-editor.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; +import { join } from 'path'; +import { createImportRuleListener } from './utils'; + +export = new class NoNlsInStandaloneEditorRule implements eslint.Rule.RuleModule { + + readonly meta: eslint.Rule.RuleMetaData = { + messages: { + noNls: 'Not allowed to import vs/nls in standalone editor modules. Use standaloneStrings.ts' + } + }; + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + + const fileName = context.getFilename(); + if ( + /vs(\/|\\)editor(\/|\\)standalone(\/|\\)/.test(fileName) + || /vs(\/|\\)editor(\/|\\)common(\/|\\)standalone(\/|\\)/.test(fileName) + || /vs(\/|\\)editor(\/|\\)editor.api/.test(fileName) + || /vs(\/|\\)editor(\/|\\)editor.main/.test(fileName) + || /vs(\/|\\)editor(\/|\\)editor.worker/.test(fileName) + ) { + return createImportRuleListener((node, path) => { + // resolve relative paths + if (path[0] === '.') { + path = join(context.getFilename(), path); + } + + if ( + /vs(\/|\\)nls/.test(path) + ) { + context.report({ + loc: node.loc, + messageId: 'noNls' + }); + } + }); + } + + return {}; + } +}; + diff --git a/build/lib/eslint/code-no-standalone-editor.js b/build/lib/eslint/code-no-standalone-editor.js new file mode 100644 index 0000000000..df97c4d7e0 --- /dev/null +++ b/build/lib/eslint/code-no-standalone-editor.js @@ -0,0 +1,41 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +const path_1 = require("path"); +const utils_1 = require("./utils"); +module.exports = new class NoNlsInStandaloneEditorRule { + constructor() { + this.meta = { + messages: { + badImport: 'Not allowed to import standalone editor modules.' + }, + docs: { + url: 'https://github.com/microsoft/vscode/wiki/Source-Code-Organization' + } + }; + } + create(context) { + if (/vs(\/|\\)editor/.test(context.getFilename())) { + // the vs/editor folder is allowed to use the standalone editor + return {}; + } + return utils_1.createImportRuleListener((node, path) => { + // resolve relative paths + if (path[0] === '.') { + path = path_1.join(context.getFilename(), path); + } + if (/vs(\/|\\)editor(\/|\\)standalone(\/|\\)/.test(path) + || /vs(\/|\\)editor(\/|\\)common(\/|\\)standalone(\/|\\)/.test(path) + || /vs(\/|\\)editor(\/|\\)editor.api/.test(path) + || /vs(\/|\\)editor(\/|\\)editor.main/.test(path) + || /vs(\/|\\)editor(\/|\\)editor.worker/.test(path)) { + context.report({ + loc: node.loc, + messageId: 'badImport' + }); + } + }); + } +}; diff --git a/build/lib/eslint/code-no-standalone-editor.ts b/build/lib/eslint/code-no-standalone-editor.ts new file mode 100644 index 0000000000..9815d61ed2 --- /dev/null +++ b/build/lib/eslint/code-no-standalone-editor.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; +import { join } from 'path'; +import { createImportRuleListener } from './utils'; + +export = new class NoNlsInStandaloneEditorRule implements eslint.Rule.RuleModule { + + readonly meta: eslint.Rule.RuleMetaData = { + messages: { + badImport: 'Not allowed to import standalone editor modules.' + }, + docs: { + url: 'https://github.com/microsoft/vscode/wiki/Source-Code-Organization' + } + }; + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + + if (/vs(\/|\\)editor/.test(context.getFilename())) { + // the vs/editor folder is allowed to use the standalone editor + return {}; + } + + return createImportRuleListener((node, path) => { + + // resolve relative paths + if (path[0] === '.') { + path = join(context.getFilename(), path); + } + + if ( + /vs(\/|\\)editor(\/|\\)standalone(\/|\\)/.test(path) + || /vs(\/|\\)editor(\/|\\)common(\/|\\)standalone(\/|\\)/.test(path) + || /vs(\/|\\)editor(\/|\\)editor.api/.test(path) + || /vs(\/|\\)editor(\/|\\)editor.main/.test(path) + || /vs(\/|\\)editor(\/|\\)editor.worker/.test(path) + ) { + context.report({ + loc: node.loc, + messageId: 'badImport' + }); + } + }); + } +}; + diff --git a/build/lib/eslint/code-no-unexternalized-strings.js b/build/lib/eslint/code-no-unexternalized-strings.js new file mode 100644 index 0000000000..ae233eba07 --- /dev/null +++ b/build/lib/eslint/code-no-unexternalized-strings.js @@ -0,0 +1,111 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +var _a; +const experimental_utils_1 = require("@typescript-eslint/experimental-utils"); +function isStringLiteral(node) { + return !!node && node.type === experimental_utils_1.AST_NODE_TYPES.Literal && typeof node.value === 'string'; +} +function isDoubleQuoted(node) { + return node.raw[0] === '"' && node.raw[node.raw.length - 1] === '"'; +} +module.exports = new (_a = class NoUnexternalizedStrings { + constructor() { + this.meta = { + messages: { + doubleQuoted: 'Only use double-quoted strings for externalized strings.', + badKey: 'The key \'{{key}}\' doesn\'t conform to a valid localize identifier.', + duplicateKey: 'Duplicate key \'{{key}}\' with different message value.', + badMessage: 'Message argument to \'{{message}}\' must be a string literal.' + } + }; + } + create(context) { + const externalizedStringLiterals = new Map(); + const doubleQuotedStringLiterals = new Set(); + function collectDoubleQuotedStrings(node) { + if (isStringLiteral(node) && isDoubleQuoted(node)) { + doubleQuotedStringLiterals.add(node); + } + } + function visitLocalizeCall(node) { + // localize(key, message) + const [keyNode, messageNode] = node.arguments; + // (1) + // extract key so that it can be checked later + let key; + if (isStringLiteral(keyNode)) { + doubleQuotedStringLiterals.delete(keyNode); //todo@joh reconsider + key = keyNode.value; + } + else if (keyNode.type === experimental_utils_1.AST_NODE_TYPES.ObjectExpression) { + for (let property of keyNode.properties) { + if (property.type === experimental_utils_1.AST_NODE_TYPES.Property && !property.computed) { + if (property.key.type === experimental_utils_1.AST_NODE_TYPES.Identifier && property.key.name === 'key') { + if (isStringLiteral(property.value)) { + doubleQuotedStringLiterals.delete(property.value); //todo@joh reconsider + key = property.value.value; + break; + } + } + } + } + } + if (typeof key === 'string') { + let array = externalizedStringLiterals.get(key); + if (!array) { + array = []; + externalizedStringLiterals.set(key, array); + } + array.push({ call: node, message: messageNode }); + } + // (2) + // remove message-argument from doubleQuoted list and make + // sure it is a string-literal + doubleQuotedStringLiterals.delete(messageNode); + if (!isStringLiteral(messageNode)) { + context.report({ + loc: messageNode.loc, + messageId: 'badMessage', + data: { message: context.getSourceCode().getText(node) } + }); + } + } + function reportBadStringsAndBadKeys() { + // (1) + // report all strings that are in double quotes + for (const node of doubleQuotedStringLiterals) { + context.report({ loc: node.loc, messageId: 'doubleQuoted' }); + } + for (const [key, values] of externalizedStringLiterals) { + // (2) + // report all invalid NLS keys + if (!key.match(NoUnexternalizedStrings._rNlsKeys)) { + for (let value of values) { + context.report({ loc: value.call.loc, messageId: 'badKey', data: { key } }); + } + } + // (2) + // report all invalid duplicates (same key, different message) + if (values.length > 1) { + for (let i = 1; i < values.length; i++) { + if (context.getSourceCode().getText(values[i - 1].message) !== context.getSourceCode().getText(values[i].message)) { + context.report({ loc: values[i].call.loc, messageId: 'duplicateKey', data: { key } }); + } + } + } + } + } + return { + ['Literal']: (node) => collectDoubleQuotedStrings(node), + ['ExpressionStatement[directive] Literal:exit']: (node) => doubleQuotedStringLiterals.delete(node), + ['CallExpression[callee.type="MemberExpression"][callee.object.name="nls"][callee.property.name="localize"]:exit']: (node) => visitLocalizeCall(node), + ['CallExpression[callee.name="localize"][arguments.length>=2]:exit']: (node) => visitLocalizeCall(node), + ['Program:exit']: reportBadStringsAndBadKeys, + }; + } + }, + _a._rNlsKeys = /^[_a-zA-Z0-9][ .\-_a-zA-Z0-9]*$/, + _a); diff --git a/build/lib/eslint/code-no-unexternalized-strings.ts b/build/lib/eslint/code-no-unexternalized-strings.ts new file mode 100644 index 0000000000..9e77bfd3f8 --- /dev/null +++ b/build/lib/eslint/code-no-unexternalized-strings.ts @@ -0,0 +1,126 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; +import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/experimental-utils'; + +function isStringLiteral(node: TSESTree.Node | null | undefined): node is TSESTree.StringLiteral { + return !!node && node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string'; +} + +function isDoubleQuoted(node: TSESTree.StringLiteral): boolean { + return node.raw[0] === '"' && node.raw[node.raw.length - 1] === '"'; +} + +export = new class NoUnexternalizedStrings implements eslint.Rule.RuleModule { + + private static _rNlsKeys = /^[_a-zA-Z0-9][ .\-_a-zA-Z0-9]*$/; + + readonly meta: eslint.Rule.RuleMetaData = { + messages: { + doubleQuoted: 'Only use double-quoted strings for externalized strings.', + badKey: 'The key \'{{key}}\' doesn\'t conform to a valid localize identifier.', + duplicateKey: 'Duplicate key \'{{key}}\' with different message value.', + badMessage: 'Message argument to \'{{message}}\' must be a string literal.' + } + }; + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + + const externalizedStringLiterals = new Map(); + const doubleQuotedStringLiterals = new Set(); + + function collectDoubleQuotedStrings(node: TSESTree.Literal) { + if (isStringLiteral(node) && isDoubleQuoted(node)) { + doubleQuotedStringLiterals.add(node); + } + } + + function visitLocalizeCall(node: TSESTree.CallExpression) { + + // localize(key, message) + const [keyNode, messageNode] = (node).arguments; + + // (1) + // extract key so that it can be checked later + let key: string | undefined; + if (isStringLiteral(keyNode)) { + doubleQuotedStringLiterals.delete(keyNode); //todo@joh reconsider + key = keyNode.value; + + } else if (keyNode.type === AST_NODE_TYPES.ObjectExpression) { + for (let property of keyNode.properties) { + if (property.type === AST_NODE_TYPES.Property && !property.computed) { + if (property.key.type === AST_NODE_TYPES.Identifier && property.key.name === 'key') { + if (isStringLiteral(property.value)) { + doubleQuotedStringLiterals.delete(property.value); //todo@joh reconsider + key = property.value.value; + break; + } + } + } + } + } + if (typeof key === 'string') { + let array = externalizedStringLiterals.get(key); + if (!array) { + array = []; + externalizedStringLiterals.set(key, array); + } + array.push({ call: node, message: messageNode }); + } + + // (2) + // remove message-argument from doubleQuoted list and make + // sure it is a string-literal + doubleQuotedStringLiterals.delete(messageNode); + if (!isStringLiteral(messageNode)) { + context.report({ + loc: messageNode.loc, + messageId: 'badMessage', + data: { message: context.getSourceCode().getText(node) } + }); + } + } + + function reportBadStringsAndBadKeys() { + // (1) + // report all strings that are in double quotes + for (const node of doubleQuotedStringLiterals) { + context.report({ loc: node.loc, messageId: 'doubleQuoted' }); + } + + for (const [key, values] of externalizedStringLiterals) { + + // (2) + // report all invalid NLS keys + if (!key.match(NoUnexternalizedStrings._rNlsKeys)) { + for (let value of values) { + context.report({ loc: value.call.loc, messageId: 'badKey', data: { key } }); + } + } + + // (2) + // report all invalid duplicates (same key, different message) + if (values.length > 1) { + for (let i = 1; i < values.length; i++) { + if (context.getSourceCode().getText(values[i - 1].message) !== context.getSourceCode().getText(values[i].message)) { + context.report({ loc: values[i].call.loc, messageId: 'duplicateKey', data: { key } }); + } + } + } + } + } + + return { + ['Literal']: (node: any) => collectDoubleQuotedStrings(node), + ['ExpressionStatement[directive] Literal:exit']: (node: any) => doubleQuotedStringLiterals.delete(node), + ['CallExpression[callee.type="MemberExpression"][callee.object.name="nls"][callee.property.name="localize"]:exit']: (node: any) => visitLocalizeCall(node), + ['CallExpression[callee.name="localize"][arguments.length>=2]:exit']: (node: any) => visitLocalizeCall(node), + ['Program:exit']: reportBadStringsAndBadKeys, + }; + } +}; + diff --git a/build/lib/eslint/code-no-unused-expressions.js b/build/lib/eslint/code-no-unused-expressions.js new file mode 100644 index 0000000000..21a29c94ea --- /dev/null +++ b/build/lib/eslint/code-no-unused-expressions.js @@ -0,0 +1,145 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// FORKED FROM https://github.com/eslint/eslint/blob/b23ad0d789a909baf8d7c41a35bc53df932eaf30/lib/rules/no-unused-expressions.js +// and added support for `OptionalCallExpression`, see https://github.com/facebook/create-react-app/issues/8107 and https://github.com/eslint/eslint/issues/12642 + +/** + * @fileoverview Flag expressions in statement position that do not side effect + * @author Michael Ficarra + */ + +'use strict'; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: 'suggestion', + + docs: { + description: 'disallow unused expressions', + category: 'Best Practices', + recommended: false, + url: 'https://eslint.org/docs/rules/no-unused-expressions' + }, + + schema: [ + { + type: 'object', + properties: { + allowShortCircuit: { + type: 'boolean', + default: false + }, + allowTernary: { + type: 'boolean', + default: false + }, + allowTaggedTemplates: { + type: 'boolean', + default: false + } + }, + additionalProperties: false + } + ] + }, + + create(context) { + const config = context.options[0] || {}, + allowShortCircuit = config.allowShortCircuit || false, + allowTernary = config.allowTernary || false, + allowTaggedTemplates = config.allowTaggedTemplates || false; + + // eslint-disable-next-line jsdoc/require-description + /** + * @param {ASTNode} node any node + * @returns {boolean} whether the given node structurally represents a directive + */ + function looksLikeDirective(node) { + return node.type === 'ExpressionStatement' && + node.expression.type === 'Literal' && typeof node.expression.value === 'string'; + } + + // eslint-disable-next-line jsdoc/require-description + /** + * @param {Function} predicate ([a] -> Boolean) the function used to make the determination + * @param {a[]} list the input list + * @returns {a[]} the leading sequence of members in the given list that pass the given predicate + */ + function takeWhile(predicate, list) { + for (let i = 0; i < list.length; ++i) { + if (!predicate(list[i])) { + return list.slice(0, i); + } + } + return list.slice(); + } + + // eslint-disable-next-line jsdoc/require-description + /** + * @param {ASTNode} node a Program or BlockStatement node + * @returns {ASTNode[]} the leading sequence of directive nodes in the given node's body + */ + function directives(node) { + return takeWhile(looksLikeDirective, node.body); + } + + // eslint-disable-next-line jsdoc/require-description + /** + * @param {ASTNode} node any node + * @param {ASTNode[]} ancestors the given node's ancestors + * @returns {boolean} whether the given node is considered a directive in its current position + */ + function isDirective(node, ancestors) { + const parent = ancestors[ancestors.length - 1], + grandparent = ancestors[ancestors.length - 2]; + + return (parent.type === 'Program' || parent.type === 'BlockStatement' && + (/Function/u.test(grandparent.type))) && + directives(parent).indexOf(node) >= 0; + } + + /** + * Determines whether or not a given node is a valid expression. Recurses on short circuit eval and ternary nodes if enabled by flags. + * @param {ASTNode} node any node + * @returns {boolean} whether the given node is a valid expression + */ + function isValidExpression(node) { + if (allowTernary) { + + // Recursive check for ternary and logical expressions + if (node.type === 'ConditionalExpression') { + return isValidExpression(node.consequent) && isValidExpression(node.alternate); + } + } + + if (allowShortCircuit) { + if (node.type === 'LogicalExpression') { + return isValidExpression(node.right); + } + } + + if (allowTaggedTemplates && node.type === 'TaggedTemplateExpression') { + return true; + } + + return /^(?:Assignment|OptionalCall|Call|New|Update|Yield|Await)Expression$/u.test(node.type) || + (node.type === 'UnaryExpression' && ['delete', 'void'].indexOf(node.operator) >= 0); + } + + return { + ExpressionStatement(node) { + if (!isValidExpression(node.expression) && !isDirective(node, context.getAncestors())) { + context.report({ node, message: 'Expected an assignment or function call and instead saw an expression.' }); + } + } + }; + + } +}; diff --git a/build/lib/eslint/code-translation-remind.js b/build/lib/eslint/code-translation-remind.js new file mode 100644 index 0000000000..01a39c82bb --- /dev/null +++ b/build/lib/eslint/code-translation-remind.js @@ -0,0 +1,57 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +var _a; +const fs_1 = require("fs"); +const utils_1 = require("./utils"); +module.exports = new (_a = class TranslationRemind { + constructor() { + this.meta = { + messages: { + missing: 'Please add \'{{resource}}\' to ./build/lib/i18n.resources.json file to use translations here.' + } + }; + } + create(context) { + return utils_1.createImportRuleListener((node, path) => this._checkImport(context, node, path)); + } + _checkImport(context, node, path) { + if (path !== TranslationRemind.NLS_MODULE) { + return; + } + const currentFile = context.getFilename(); + const matchService = currentFile.match(/vs\/workbench\/services\/\w+/); + const matchPart = currentFile.match(/vs\/workbench\/contrib\/\w+/); + if (!matchService && !matchPart) { + return; + } + const resource = matchService ? matchService[0] : matchPart[0]; + let resourceDefined = false; + let json; + try { + json = fs_1.readFileSync('./build/lib/i18n.resources.json', 'utf8'); + } + catch (e) { + console.error('[translation-remind rule]: File with resources to pull from Transifex was not found. Aborting translation resource check for newly defined workbench part/service.'); + return; + } + const workbenchResources = JSON.parse(json).workbench; + workbenchResources.forEach((existingResource) => { + if (existingResource.name === resource) { + resourceDefined = true; + return; + } + }); + if (!resourceDefined) { + context.report({ + loc: node.loc, + messageId: 'missing', + data: { resource } + }); + } + } + }, + _a.NLS_MODULE = 'vs/nls', + _a); diff --git a/build/lib/eslint/code-translation-remind.ts b/build/lib/eslint/code-translation-remind.ts new file mode 100644 index 0000000000..b2b1304810 --- /dev/null +++ b/build/lib/eslint/code-translation-remind.ts @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; +import { TSESTree } from '@typescript-eslint/experimental-utils'; +import { readFileSync } from 'fs'; +import { createImportRuleListener } from './utils'; + + +export = new class TranslationRemind implements eslint.Rule.RuleModule { + + private static NLS_MODULE = 'vs/nls'; + + readonly meta: eslint.Rule.RuleMetaData = { + messages: { + missing: 'Please add \'{{resource}}\' to ./build/lib/i18n.resources.json file to use translations here.' + } + }; + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + return createImportRuleListener((node, path) => this._checkImport(context, node, path)); + } + + private _checkImport(context: eslint.Rule.RuleContext, node: TSESTree.Node, path: string) { + + if (path !== TranslationRemind.NLS_MODULE) { + return; + } + + const currentFile = context.getFilename(); + const matchService = currentFile.match(/vs\/workbench\/services\/\w+/); + const matchPart = currentFile.match(/vs\/workbench\/contrib\/\w+/); + if (!matchService && !matchPart) { + return; + } + + const resource = matchService ? matchService[0] : matchPart![0]; + let resourceDefined = false; + + let json; + try { + json = readFileSync('./build/lib/i18n.resources.json', 'utf8'); + } catch (e) { + console.error('[translation-remind rule]: File with resources to pull from Transifex was not found. Aborting translation resource check for newly defined workbench part/service.'); + return; + } + const workbenchResources = JSON.parse(json).workbench; + + workbenchResources.forEach((existingResource: any) => { + if (existingResource.name === resource) { + resourceDefined = true; + return; + } + }); + + if (!resourceDefined) { + context.report({ + loc: node.loc, + messageId: 'missing', + data: { resource } + }); + } + } +}; + diff --git a/build/lib/eslint/utils.js b/build/lib/eslint/utils.js new file mode 100644 index 0000000000..2a3d952897 --- /dev/null +++ b/build/lib/eslint/utils.js @@ -0,0 +1,36 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +function createImportRuleListener(validateImport) { + function _checkImport(node) { + if (node && node.type === 'Literal' && typeof node.value === 'string') { + validateImport(node, node.value); + } + } + return { + // import ??? from 'module' + ImportDeclaration: (node) => { + _checkImport(node.source); + }, + // import('module').then(...) OR await import('module') + ['CallExpression[callee.type="Import"][arguments.length=1] > Literal']: (node) => { + _checkImport(node); + }, + // import foo = ... + ['TSImportEqualsDeclaration > TSExternalModuleReference > Literal']: (node) => { + _checkImport(node); + }, + // export ?? from 'module' + ExportAllDeclaration: (node) => { + _checkImport(node.source); + }, + // export {foo} from 'module' + ExportNamedDeclaration: (node) => { + _checkImport(node.source); + }, + }; +} +exports.createImportRuleListener = createImportRuleListener; diff --git a/build/lib/eslint/utils.ts b/build/lib/eslint/utils.ts new file mode 100644 index 0000000000..d9f88cc17a --- /dev/null +++ b/build/lib/eslint/utils.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; +import { TSESTree } from '@typescript-eslint/experimental-utils'; + +export function createImportRuleListener(validateImport: (node: TSESTree.Literal, value: string) => any): eslint.Rule.RuleListener { + + function _checkImport(node: TSESTree.Node | null) { + if (node && node.type === 'Literal' && typeof node.value === 'string') { + validateImport(node, node.value); + } + } + + return { + // import ??? from 'module' + ImportDeclaration: (node: any) => { + _checkImport((node).source); + }, + // import('module').then(...) OR await import('module') + ['CallExpression[callee.type="Import"][arguments.length=1] > Literal']: (node: any) => { + _checkImport(node); + }, + // import foo = ... + ['TSImportEqualsDeclaration > TSExternalModuleReference > Literal']: (node: any) => { + _checkImport(node); + }, + // export ?? from 'module' + ExportAllDeclaration: (node: any) => { + _checkImport((node).source); + }, + // export {foo} from 'module' + ExportNamedDeclaration: (node: any) => { + _checkImport((node).source); + }, + + }; +} diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index d5fd9c166b..174083dac8 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -30,6 +30,14 @@ "name": "vs/workbench/api/common", "project": "vscode-workbench" }, + { + "name": "vs/workbench/contrib/backup", + "project": "vscode-workbench" + }, + { + "name": "vs/workbench/contrib/bulkEdit", + "project": "vscode-workbench" + }, { "name": "vs/workbench/contrib/cli", "project": "vscode-workbench" @@ -309,6 +317,10 @@ { "name": "vs/workbench/services/userData", "project": "vscode-workbench" + }, + { + "name": "vs/workbench/services/userDataSync", + "project": "vscode-workbench" } ] } diff --git a/build/lib/layersChecker.js b/build/lib/layersChecker.js new file mode 100644 index 0000000000..7a0e12a615 --- /dev/null +++ b/build/lib/layersChecker.js @@ -0,0 +1,209 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +const ts = require("typescript"); +const fs_1 = require("fs"); +const path_1 = require("path"); +const minimatch_1 = require("minimatch"); +// +// ############################################################################################# +// +// A custom typescript checker for the specific task of detecting the use of certain types in a +// layer that does not allow such use. For example: +// - using DOM globals in common/node/electron-main layer (e.g. HTMLElement) +// - using node.js globals in common/browser layer (e.g. process) +// +// Make changes to below RULES to lift certain files from these checks only if absolutely needed +// +// ############################################################################################# +// +// Types we assume are present in all implementations of JS VMs (node.js, browsers) +// Feel free to add more core types as you see needed if present in node.js and browsers +const CORE_TYPES = [ + 'require', + 'atob', + 'btoa', + 'setTimeout', + 'clearTimeout', + 'setInterval', + 'clearInterval', + 'console', + 'log', + 'info', + 'warn', + 'error', + 'group', + 'groupEnd', + 'table', + 'Error', + 'String', + 'throws', + 'stack', + 'captureStackTrace', + 'stackTraceLimit', + 'TextDecoder', + 'TextEncoder', + 'encode', + 'decode', + 'self', + 'trimLeft', + 'trimRight' +]; +const RULES = [ + // Tests: skip + { + target: '**/{vs,sql}/**/test/**', + skip: true // -> skip all test files + }, + // Common: vs/base/common/platform.ts + { + target: '**/vs/base/common/platform.ts', + allowedTypes: [ + ...CORE_TYPES, + // Safe access to postMessage() and friends + 'MessageEvent', + 'data' + ], + disallowedDefinitions: [ + 'lib.dom.d.ts', + '@types/node' // no node.js + ] + }, + // Common: vs/workbench/api/common/extHostExtensionService.ts + { + target: '**/vs/workbench/api/common/extHostExtensionService.ts', + allowedTypes: [ + ...CORE_TYPES, + // Safe access to global + 'global' + ], + disallowedDefinitions: [ + 'lib.dom.d.ts', + '@types/node' // no node.js + ] + }, + // Common + { + target: '**/{vs,sql}/**/common/**', + allowedTypes: CORE_TYPES, + disallowedDefinitions: [ + 'lib.dom.d.ts', + '@types/node' // no node.js + ] + }, + // Browser + { + target: '**/{vs,sql}/**/browser/**', + allowedTypes: CORE_TYPES, + disallowedDefinitions: [ + '@types/node' // no node.js + ] + }, + // node.js + { + target: '**/{vs,sql}/**/node/**', + allowedTypes: [ + ...CORE_TYPES, + // --> types from node.d.ts that duplicate from lib.dom.d.ts + 'URL', + 'protocol', + 'hostname', + 'port', + 'pathname', + 'search', + 'username', + 'password' + ], + disallowedDefinitions: [ + 'lib.dom.d.ts' // no DOM + ] + }, + // Electron (renderer): skip + { + target: '**/{vs,sql}/**/electron-browser/**', + skip: true // -> supports all types + }, + // Electron (main) + { + target: '**/{vs,sql}/**/electron-main/**', + allowedTypes: [ + ...CORE_TYPES, + // --> types from electron.d.ts that duplicate from lib.dom.d.ts + 'Event', + 'Request' + ], + disallowedDefinitions: [ + 'lib.dom.d.ts' // no DOM + ] + } +]; +const TS_CONFIG_PATH = path_1.join(__dirname, '../../', 'src', 'tsconfig.json'); +let hasErrors = false; +function checkFile(program, sourceFile, rule) { + checkNode(sourceFile); + function checkNode(node) { + var _a; + if (node.kind !== ts.SyntaxKind.Identifier) { + return ts.forEachChild(node, checkNode); // recurse down + } + const text = node.getText(sourceFile); + if ((_a = rule.allowedTypes) === null || _a === void 0 ? void 0 : _a.some(allowed => allowed === text)) { + return; // override + } + const checker = program.getTypeChecker(); + const symbol = checker.getSymbolAtLocation(node); + if (symbol) { + const declarations = symbol.declarations; + if (Array.isArray(declarations)) { + for (const declaration of declarations) { + if (declaration) { + const parent = declaration.parent; + if (parent) { + const parentSourceFile = parent.getSourceFile(); + if (parentSourceFile) { + const definitionFileName = parentSourceFile.fileName; + if (rule.disallowedDefinitions) { + for (const disallowedDefinition of rule.disallowedDefinitions) { + if (definitionFileName.indexOf(disallowedDefinition) >= 0) { + const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart()); + console.log(`[build/lib/layersChecker.ts]: Reference to '${text}' from '${disallowedDefinition}' violates layer '${rule.target}' (${sourceFile.fileName} (${line + 1},${character + 1})`); + hasErrors = true; + return; + } + } + } + } + } + } + } + } + } + } +} +function createProgram(tsconfigPath) { + const tsConfig = ts.readConfigFile(tsconfigPath, ts.sys.readFile); + const configHostParser = { fileExists: fs_1.existsSync, readDirectory: ts.sys.readDirectory, readFile: file => fs_1.readFileSync(file, 'utf8'), useCaseSensitiveFileNames: process.platform === 'linux' }; + const tsConfigParsed = ts.parseJsonConfigFileContent(tsConfig.config, configHostParser, path_1.resolve(path_1.dirname(tsconfigPath)), { noEmit: true }); + const compilerHost = ts.createCompilerHost(tsConfigParsed.options, true); + return ts.createProgram(tsConfigParsed.fileNames, tsConfigParsed.options, compilerHost); +} +// +// Create program and start checking +// +const program = createProgram(TS_CONFIG_PATH); +for (const sourceFile of program.getSourceFiles()) { + for (const rule of RULES) { + if (minimatch_1.match([sourceFile.fileName], rule.target).length > 0) { + if (!rule.skip) { + checkFile(program, sourceFile, rule); + } + break; + } + } +} +if (hasErrors) { + process.exit(1); +} diff --git a/build/lib/layersChecker.ts b/build/lib/layersChecker.ts new file mode 100644 index 0000000000..827064c501 --- /dev/null +++ b/build/lib/layersChecker.ts @@ -0,0 +1,245 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as ts from 'typescript'; +import { readFileSync, existsSync } from 'fs'; +import { resolve, dirname, join } from 'path'; +import { match } from 'minimatch'; + +// +// ############################################################################################# +// +// A custom typescript checker for the specific task of detecting the use of certain types in a +// layer that does not allow such use. For example: +// - using DOM globals in common/node/electron-main layer (e.g. HTMLElement) +// - using node.js globals in common/browser layer (e.g. process) +// +// Make changes to below RULES to lift certain files from these checks only if absolutely needed +// +// ############################################################################################# +// + +// Types we assume are present in all implementations of JS VMs (node.js, browsers) +// Feel free to add more core types as you see needed if present in node.js and browsers +const CORE_TYPES = [ + 'require', // from our AMD loader + 'atob', + 'btoa', + 'setTimeout', + 'clearTimeout', + 'setInterval', + 'clearInterval', + 'console', + 'log', + 'info', + 'warn', + 'error', + 'group', + 'groupEnd', + 'table', + 'Error', + 'String', + 'throws', + 'stack', + 'captureStackTrace', + 'stackTraceLimit', + 'TextDecoder', + 'TextEncoder', + 'encode', + 'decode', + 'self', + 'trimLeft', + 'trimRight' +]; + +const RULES = [ + + // Tests: skip + { + target: '**/{vs,sql}/**/test/**', + skip: true // -> skip all test files + }, + + // Common: vs/base/common/platform.ts + { + target: '**/vs/base/common/platform.ts', + allowedTypes: [ + ...CORE_TYPES, + + // Safe access to postMessage() and friends + 'MessageEvent', + 'data' + ], + disallowedDefinitions: [ + 'lib.dom.d.ts', // no DOM + '@types/node' // no node.js + ] + }, + + // Common: vs/workbench/api/common/extHostExtensionService.ts + { + target: '**/vs/workbench/api/common/extHostExtensionService.ts', + allowedTypes: [ + ...CORE_TYPES, + + // Safe access to global + 'global' + ], + disallowedDefinitions: [ + 'lib.dom.d.ts', // no DOM + '@types/node' // no node.js + ] + }, + + // Common + { + target: '**/{vs,sql}/**/common/**', + allowedTypes: CORE_TYPES, + disallowedDefinitions: [ + 'lib.dom.d.ts', // no DOM + '@types/node' // no node.js + ] + }, + + // Browser + { + target: '**/{vs,sql}/**/browser/**', + allowedTypes: CORE_TYPES, + disallowedDefinitions: [ + '@types/node' // no node.js + ] + }, + + // node.js + { + target: '**/{vs,sql}/**/node/**', + allowedTypes: [ + ...CORE_TYPES, + + // --> types from node.d.ts that duplicate from lib.dom.d.ts + 'URL', + 'protocol', + 'hostname', + 'port', + 'pathname', + 'search', + 'username', + 'password' + ], + disallowedDefinitions: [ + 'lib.dom.d.ts' // no DOM + ] + }, + + // Electron (renderer): skip + { + target: '**/{vs,sql}/**/electron-browser/**', + skip: true // -> supports all types + }, + + // Electron (main) + { + target: '**/{vs,sql}/**/electron-main/**', + allowedTypes: [ + ...CORE_TYPES, + + // --> types from electron.d.ts that duplicate from lib.dom.d.ts + 'Event', + 'Request' + ], + disallowedDefinitions: [ + 'lib.dom.d.ts' // no DOM + ] + } +]; + +const TS_CONFIG_PATH = join(__dirname, '../../', 'src', 'tsconfig.json'); + +interface IRule { + target: string; + skip?: boolean; + allowedTypes?: string[]; + disallowedDefinitions?: string[]; +} + +let hasErrors = false; + +function checkFile(program: ts.Program, sourceFile: ts.SourceFile, rule: IRule) { + checkNode(sourceFile); + + function checkNode(node: ts.Node): void { + if (node.kind !== ts.SyntaxKind.Identifier) { + return ts.forEachChild(node, checkNode); // recurse down + } + + const text = node.getText(sourceFile); + + if (rule.allowedTypes?.some(allowed => allowed === text)) { + return; // override + } + + const checker = program.getTypeChecker(); + const symbol = checker.getSymbolAtLocation(node); + if (symbol) { + const declarations = symbol.declarations; + if (Array.isArray(declarations)) { + for (const declaration of declarations) { + if (declaration) { + const parent = declaration.parent; + if (parent) { + const parentSourceFile = parent.getSourceFile(); + if (parentSourceFile) { + const definitionFileName = parentSourceFile.fileName; + if (rule.disallowedDefinitions) { + for (const disallowedDefinition of rule.disallowedDefinitions) { + if (definitionFileName.indexOf(disallowedDefinition) >= 0) { + const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart()); + console.log(`[build/lib/layersChecker.ts]: Reference to '${text}' from '${disallowedDefinition}' violates layer '${rule.target}' (${sourceFile.fileName} (${line + 1},${character + 1})`); + + hasErrors = true; + return; + } + } + } + } + } + } + } + } + } + } +} + +function createProgram(tsconfigPath: string): ts.Program { + const tsConfig = ts.readConfigFile(tsconfigPath, ts.sys.readFile); + + const configHostParser: ts.ParseConfigHost = { fileExists: existsSync, readDirectory: ts.sys.readDirectory, readFile: file => readFileSync(file, 'utf8'), useCaseSensitiveFileNames: process.platform === 'linux' }; + const tsConfigParsed = ts.parseJsonConfigFileContent(tsConfig.config, configHostParser, resolve(dirname(tsconfigPath)), { noEmit: true }); + + const compilerHost = ts.createCompilerHost(tsConfigParsed.options, true); + + return ts.createProgram(tsConfigParsed.fileNames, tsConfigParsed.options, compilerHost); +} + +// +// Create program and start checking +// +const program = createProgram(TS_CONFIG_PATH); + +for (const sourceFile of program.getSourceFiles()) { + for (const rule of RULES) { + if (match([sourceFile.fileName], rule.target).length > 0) { + if (!rule.skip) { + checkFile(program, sourceFile, rule); + } + + break; + } + } +} + +if (hasErrors) { + process.exit(1); +} diff --git a/build/lib/tslint/abstractGlobalsRule.js b/build/lib/tslint/abstractGlobalsRule.js deleted file mode 100644 index 76e673284b..0000000000 --- a/build/lib/tslint/abstractGlobalsRule.js +++ /dev/null @@ -1,45 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -const Lint = require("tslint"); -class AbstractGlobalsRuleWalker extends Lint.RuleWalker { - constructor(file, program, opts, _config) { - super(file, opts); - this.program = program; - this._config = _config; - } - visitIdentifier(node) { - if (this.getDisallowedGlobals().some(disallowedGlobal => disallowedGlobal === node.text)) { - if (this._config.allowed && this._config.allowed.some(allowed => allowed === node.text)) { - return; // override - } - const checker = this.program.getTypeChecker(); - const symbol = checker.getSymbolAtLocation(node); - if (symbol) { - const declarations = symbol.declarations; - if (Array.isArray(declarations) && symbol.declarations.some(declaration => { - if (declaration) { - const parent = declaration.parent; - if (parent) { - const sourceFile = parent.getSourceFile(); - if (sourceFile) { - const fileName = sourceFile.fileName; - if (fileName && fileName.indexOf(this.getDefinitionPattern()) >= 0) { - return true; - } - } - } - } - return false; - })) { - this.addFailureAtNode(node, `Cannot use global '${node.text}' in '${this._config.target}'`); - } - } - } - super.visitIdentifier(node); - } -} -exports.AbstractGlobalsRuleWalker = AbstractGlobalsRuleWalker; diff --git a/build/lib/tslint/abstractGlobalsRule.ts b/build/lib/tslint/abstractGlobalsRule.ts deleted file mode 100644 index f1d1eda0f6..0000000000 --- a/build/lib/tslint/abstractGlobalsRule.ts +++ /dev/null @@ -1,57 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as ts from 'typescript'; -import * as Lint from 'tslint'; - -interface AbstractGlobalsRuleConfig { - target: string; - allowed: string[]; -} - -export abstract class AbstractGlobalsRuleWalker extends Lint.RuleWalker { - - constructor(file: ts.SourceFile, private program: ts.Program, opts: Lint.IOptions, private _config: AbstractGlobalsRuleConfig) { - super(file, opts); - } - - protected abstract getDisallowedGlobals(): string[]; - - protected abstract getDefinitionPattern(): string; - - visitIdentifier(node: ts.Identifier) { - if (this.getDisallowedGlobals().some(disallowedGlobal => disallowedGlobal === node.text)) { - if (this._config.allowed && this._config.allowed.some(allowed => allowed === node.text)) { - return; // override - } - - const checker = this.program.getTypeChecker(); - const symbol = checker.getSymbolAtLocation(node); - if (symbol) { - const declarations = symbol.declarations; - if (Array.isArray(declarations) && symbol.declarations.some(declaration => { - if (declaration) { - const parent = declaration.parent; - if (parent) { - const sourceFile = parent.getSourceFile(); - if (sourceFile) { - const fileName = sourceFile.fileName; - if (fileName && fileName.indexOf(this.getDefinitionPattern()) >= 0) { - return true; - } - } - } - } - - return false; - })) { - this.addFailureAtNode(node, `Cannot use global '${node.text}' in '${this._config.target}'`); - } - } - } - - super.visitIdentifier(node); - } -} diff --git a/build/lib/tslint/doubleQuotedStringArgRule.js b/build/lib/tslint/doubleQuotedStringArgRule.js deleted file mode 100644 index 94c736e60f..0000000000 --- a/build/lib/tslint/doubleQuotedStringArgRule.js +++ /dev/null @@ -1,58 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -const ts = require("typescript"); -const Lint = require("tslint"); -/** - * Implementation of the double-quoted-string-arg rule which verifies that the specified index of calls matching - * the specified signatures is quoted with double-quotes only. - */ -class Rule extends Lint.Rules.AbstractRule { - apply(sourceFile) { - return this.applyWithWalker(new DoubleQuotedStringArgRuleWalker(sourceFile, this.getOptions())); - } -} -exports.Rule = Rule; -class DoubleQuotedStringArgRuleWalker extends Lint.RuleWalker { - constructor(file, opts) { - super(file, opts); - this.signatures = Object.create(null); - this.argIndex = undefined; - const options = this.getOptions(); - const first = options && options.length > 0 ? options[0] : null; - if (first) { - if (Array.isArray(first.signatures)) { - first.signatures.forEach((signature) => this.signatures[signature] = true); - } - if (typeof first.argIndex !== 'undefined') { - this.argIndex = first.argIndex; - } - } - } - visitCallExpression(node) { - this.checkCallExpression(node); - super.visitCallExpression(node); - } - checkCallExpression(node) { - // Not one of the functions we're looking for, continue on - const functionName = node.expression.getText(); - if (functionName && !this.signatures[functionName]) { - return; - } - const arg = node.arguments[this.argIndex]; - // Ignore if the arg isn't a string - we expect the compiler to warn if that's an issue - if (arg && ts.isStringLiteral(arg)) { - const argText = arg.getText(); - const doubleQuotedArg = argText.length >= 2 && argText[0] === DoubleQuotedStringArgRuleWalker.DOUBLE_QUOTE && argText[argText.length - 1] === DoubleQuotedStringArgRuleWalker.DOUBLE_QUOTE; - if (!doubleQuotedArg) { - const fix = Lint.Replacement.replaceFromTo(arg.getStart(), arg.getEnd(), `"${arg.getText().slice(1, arg.getWidth() - 1)}"`); - this.addFailure(this.createFailure(arg.getStart(), arg.getWidth(), `Argument ${this.argIndex + 1} to '${functionName}' must be double quoted.`, fix)); - return; - } - } - } -} -DoubleQuotedStringArgRuleWalker.DOUBLE_QUOTE = '"'; diff --git a/build/lib/tslint/doubleQuotedStringArgRule.ts b/build/lib/tslint/doubleQuotedStringArgRule.ts deleted file mode 100644 index 07f01dbcb3..0000000000 --- a/build/lib/tslint/doubleQuotedStringArgRule.ts +++ /dev/null @@ -1,80 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as ts from 'typescript'; -import * as Lint from 'tslint'; - -/** - * Implementation of the double-quoted-string-arg rule which verifies that the specified index of calls matching - * the specified signatures is quoted with double-quotes only. - */ -export class Rule extends Lint.Rules.AbstractRule { - public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - return this.applyWithWalker(new DoubleQuotedStringArgRuleWalker(sourceFile, this.getOptions())); - } -} - -interface Map { - [key: string]: V; -} - -interface DoubleQuotedStringArgOptions { - signatures?: string[]; - argIndex?: number; -} - -class DoubleQuotedStringArgRuleWalker extends Lint.RuleWalker { - - private static DOUBLE_QUOTE: string = '"'; - - private signatures: Map; - private argIndex: number | undefined; - - constructor(file: ts.SourceFile, opts: Lint.IOptions) { - super(file, opts); - this.signatures = Object.create(null); - this.argIndex = undefined; - const options: any[] = this.getOptions(); - const first: DoubleQuotedStringArgOptions = options && options.length > 0 ? options[0] : null; - if (first) { - if (Array.isArray(first.signatures)) { - first.signatures.forEach((signature: string) => this.signatures[signature] = true); - } - if (typeof first.argIndex !== 'undefined') { - this.argIndex = first.argIndex; - } - } - } - - protected visitCallExpression(node: ts.CallExpression): void { - this.checkCallExpression(node); - super.visitCallExpression(node); - } - - private checkCallExpression(node: ts.CallExpression): void { - // Not one of the functions we're looking for, continue on - const functionName = node.expression.getText(); - if (functionName && !this.signatures[functionName]) { - return; - } - - const arg = node.arguments[this.argIndex!]; - - // Ignore if the arg isn't a string - we expect the compiler to warn if that's an issue - if(arg && ts.isStringLiteral(arg)) { - const argText = arg.getText(); - const doubleQuotedArg = argText.length >= 2 && argText[0] === DoubleQuotedStringArgRuleWalker.DOUBLE_QUOTE && argText[argText.length - 1] === DoubleQuotedStringArgRuleWalker.DOUBLE_QUOTE; - - if (!doubleQuotedArg) { - const fix = Lint.Replacement.replaceFromTo(arg.getStart(), arg.getEnd(), `"${arg.getText().slice(1, arg.getWidth() - 1)}"`); - this.addFailure(this.createFailure( - arg.getStart(), arg.getWidth(), - `Argument ${this.argIndex! + 1} to '${functionName}' must be double quoted.`, fix)); - return; - } - } - - } -} diff --git a/build/lib/tslint/duplicateImportsRule.js b/build/lib/tslint/duplicateImportsRule.js deleted file mode 100644 index befb01dbb9..0000000000 --- a/build/lib/tslint/duplicateImportsRule.js +++ /dev/null @@ -1,32 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -const path_1 = require("path"); -const Lint = require("tslint"); -class Rule extends Lint.Rules.AbstractRule { - apply(sourceFile) { - return this.applyWithWalker(new ImportPatterns(sourceFile, this.getOptions())); - } -} -exports.Rule = Rule; -class ImportPatterns extends Lint.RuleWalker { - constructor(file, opts) { - super(file, opts); - this.imports = Object.create(null); - } - visitImportDeclaration(node) { - let path = node.moduleSpecifier.getText(); - // remove quotes - path = path.slice(1, -1); - if (path[0] === '.') { - path = path_1.join(path_1.dirname(node.getSourceFile().fileName), path); - } - if (this.imports[path]) { - this.addFailure(this.createFailure(node.getStart(), node.getWidth(), `Duplicate imports for '${path}'.`)); - } - this.imports[path] = true; - } -} diff --git a/build/lib/tslint/duplicateImportsRule.ts b/build/lib/tslint/duplicateImportsRule.ts deleted file mode 100644 index 8c7b75fe3b..0000000000 --- a/build/lib/tslint/duplicateImportsRule.ts +++ /dev/null @@ -1,40 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as ts from 'typescript'; -import { join, dirname } from 'path'; -import * as Lint from 'tslint'; - -export class Rule extends Lint.Rules.AbstractRule { - public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - return this.applyWithWalker(new ImportPatterns(sourceFile, this.getOptions())); - } -} - -class ImportPatterns extends Lint.RuleWalker { - - private imports: { [path: string]: boolean; } = Object.create(null); - - constructor(file: ts.SourceFile, opts: Lint.IOptions) { - super(file, opts); - } - - protected visitImportDeclaration(node: ts.ImportDeclaration): void { - let path = node.moduleSpecifier.getText(); - - // remove quotes - path = path.slice(1, -1); - - if (path[0] === '.') { - path = join(dirname(node.getSourceFile().fileName), path); - } - - if (this.imports[path]) { - this.addFailure(this.createFailure(node.getStart(), node.getWidth(), `Duplicate imports for '${path}'.`)); - } - - this.imports[path] = true; - } -} diff --git a/build/lib/tslint/importPatternsRule.js b/build/lib/tslint/importPatternsRule.js deleted file mode 100644 index 39d5b045c5..0000000000 --- a/build/lib/tslint/importPatternsRule.js +++ /dev/null @@ -1,70 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -const ts = require("typescript"); -const Lint = require("tslint"); -const minimatch = require("minimatch"); -const path_1 = require("path"); -class Rule extends Lint.Rules.AbstractRule { - apply(sourceFile) { - const configs = this.getOptions().ruleArguments; - for (const config of configs) { - if (minimatch(sourceFile.fileName, config.target)) { - return this.applyWithWalker(new ImportPatterns(sourceFile, this.getOptions(), config)); - } - } - return []; - } -} -exports.Rule = Rule; -class ImportPatterns extends Lint.RuleWalker { - constructor(file, opts, _config) { - super(file, opts); - this._config = _config; - } - visitImportEqualsDeclaration(node) { - if (node.moduleReference.kind === ts.SyntaxKind.ExternalModuleReference) { - this._validateImport(node.moduleReference.expression.getText(), node); - } - } - visitImportDeclaration(node) { - this._validateImport(node.moduleSpecifier.getText(), node); - } - visitCallExpression(node) { - super.visitCallExpression(node); - // import('foo') statements inside the code - if (node.expression.kind === ts.SyntaxKind.ImportKeyword) { - const [path] = node.arguments; - this._validateImport(path.getText(), node); - } - } - _validateImport(path, node) { - // remove quotes - path = path.slice(1, -1); - // resolve relative paths - if (path[0] === '.') { - path = path_1.join(this.getSourceFile().fileName, path); - } - let restrictions; - if (typeof this._config.restrictions === 'string') { - restrictions = [this._config.restrictions]; - } - else { - restrictions = this._config.restrictions; - } - let matched = false; - for (const pattern of restrictions) { - if (minimatch(path, pattern)) { - matched = true; - break; - } - } - if (!matched) { - // None of the restrictions matched - this.addFailure(this.createFailure(node.getStart(), node.getWidth(), `Imports violates '${restrictions.join(' or ')}' restrictions. See https://github.com/Microsoft/vscode/wiki/Code-Organization`)); - } - } -} diff --git a/build/lib/tslint/importPatternsRule.ts b/build/lib/tslint/importPatternsRule.ts deleted file mode 100644 index 490e119ae5..0000000000 --- a/build/lib/tslint/importPatternsRule.ts +++ /dev/null @@ -1,87 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as ts from 'typescript'; -import * as Lint from 'tslint'; -import * as minimatch from 'minimatch'; -import { join } from 'path'; - -interface ImportPatternsConfig { - target: string; - restrictions: string | string[]; -} - -export class Rule extends Lint.Rules.AbstractRule { - public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - - const configs = this.getOptions().ruleArguments; - - - for (const config of configs) { - if (minimatch(sourceFile.fileName, config.target)) { - return this.applyWithWalker(new ImportPatterns(sourceFile, this.getOptions(), config)); - } - } - - return []; - } -} - -class ImportPatterns extends Lint.RuleWalker { - - constructor(file: ts.SourceFile, opts: Lint.IOptions, private _config: ImportPatternsConfig) { - super(file, opts); - } - - protected visitImportEqualsDeclaration(node: ts.ImportEqualsDeclaration): void { - if (node.moduleReference.kind === ts.SyntaxKind.ExternalModuleReference) { - this._validateImport(node.moduleReference.expression.getText(), node); - } - } - - protected visitImportDeclaration(node: ts.ImportDeclaration): void { - this._validateImport(node.moduleSpecifier.getText(), node); - } - - protected visitCallExpression(node: ts.CallExpression): void { - super.visitCallExpression(node); - - // import('foo') statements inside the code - if (node.expression.kind === ts.SyntaxKind.ImportKeyword) { - const [path] = node.arguments; - this._validateImport(path.getText(), node); - } - } - - private _validateImport(path: string, node: ts.Node): void { - // remove quotes - path = path.slice(1, -1); - - // resolve relative paths - if (path[0] === '.') { - path = join(this.getSourceFile().fileName, path); - } - - let restrictions: string[]; - if (typeof this._config.restrictions === 'string') { - restrictions = [this._config.restrictions]; - } else { - restrictions = this._config.restrictions; - } - - let matched = false; - for (const pattern of restrictions) { - if (minimatch(path, pattern)) { - matched = true; - break; - } - } - - if (!matched) { - // None of the restrictions matched - this.addFailure(this.createFailure(node.getStart(), node.getWidth(), `Imports violates '${restrictions.join(' or ')}' restrictions. See https://github.com/Microsoft/vscode/wiki/Code-Organization`)); - } - } -} diff --git a/build/lib/tslint/layeringRule.js b/build/lib/tslint/layeringRule.js deleted file mode 100644 index aabf98e33f..0000000000 --- a/build/lib/tslint/layeringRule.js +++ /dev/null @@ -1,83 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -const ts = require("typescript"); -const Lint = require("tslint"); -const path_1 = require("path"); -class Rule extends Lint.Rules.AbstractRule { - apply(sourceFile) { - const parts = path_1.dirname(sourceFile.fileName).split(/\\|\//); - const ruleArgs = this.getOptions().ruleArguments[0]; - let config; - for (let i = parts.length - 1; i >= 0; i--) { - if (ruleArgs[parts[i]]) { - config = { - allowed: new Set(ruleArgs[parts[i]]).add(parts[i]), - disallowed: new Set() - }; - Object.keys(ruleArgs).forEach(key => { - if (!config.allowed.has(key)) { - config.disallowed.add(key); - } - }); - break; - } - } - if (!config) { - return []; - } - return this.applyWithWalker(new LayeringRule(sourceFile, config, this.getOptions())); - } -} -exports.Rule = Rule; -class LayeringRule extends Lint.RuleWalker { - constructor(file, config, opts) { - super(file, opts); - this._config = config; - } - visitImportEqualsDeclaration(node) { - if (node.moduleReference.kind === ts.SyntaxKind.ExternalModuleReference) { - this._validateImport(node.moduleReference.expression.getText(), node); - } - } - visitImportDeclaration(node) { - this._validateImport(node.moduleSpecifier.getText(), node); - } - visitCallExpression(node) { - super.visitCallExpression(node); - // import('foo') statements inside the code - if (node.expression.kind === ts.SyntaxKind.ImportKeyword) { - const [path] = node.arguments; - this._validateImport(path.getText(), node); - } - } - _validateImport(path, node) { - // remove quotes - path = path.slice(1, -1); - if (path[0] === '.') { - path = path_1.join(path_1.dirname(node.getSourceFile().fileName), path); - } - const parts = path_1.dirname(path).split(/\\|\//); - for (let i = parts.length - 1; i >= 0; i--) { - const part = parts[i]; - if (this._config.allowed.has(part)) { - // GOOD - same layer - return; - } - if (this._config.disallowed.has(part)) { - // BAD - wrong layer - const message = `Bad layering. You are not allowed to access '${part}' from here, allowed layers are: [${LayeringRule._print(this._config.allowed)}]`; - this.addFailure(this.createFailure(node.getStart(), node.getWidth(), message)); - return; - } - } - } - static _print(set) { - const r = []; - set.forEach(e => r.push(e)); - return r.join(', '); - } -} diff --git a/build/lib/tslint/layeringRule.ts b/build/lib/tslint/layeringRule.ts deleted file mode 100644 index f63aedb5bb..0000000000 --- a/build/lib/tslint/layeringRule.ts +++ /dev/null @@ -1,105 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as ts from 'typescript'; -import * as Lint from 'tslint'; -import { join, dirname } from 'path'; - -interface Config { - allowed: Set; - disallowed: Set; -} - -export class Rule extends Lint.Rules.AbstractRule { - public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - - const parts = dirname(sourceFile.fileName).split(/\\|\//); - const ruleArgs = this.getOptions().ruleArguments[0]; - - let config: Config | undefined; - for (let i = parts.length - 1; i >= 0; i--) { - if (ruleArgs[parts[i]]) { - config = { - allowed: new Set(ruleArgs[parts[i]]).add(parts[i]), - disallowed: new Set() - }; - Object.keys(ruleArgs).forEach(key => { - if (!config!.allowed.has(key)) { - config!.disallowed.add(key); - } - }); - break; - } - } - - if (!config) { - return []; - } - - return this.applyWithWalker(new LayeringRule(sourceFile, config, this.getOptions())); - } -} - -class LayeringRule extends Lint.RuleWalker { - - private _config: Config; - - constructor(file: ts.SourceFile, config: Config, opts: Lint.IOptions) { - super(file, opts); - this._config = config; - } - - protected visitImportEqualsDeclaration(node: ts.ImportEqualsDeclaration): void { - if (node.moduleReference.kind === ts.SyntaxKind.ExternalModuleReference) { - this._validateImport(node.moduleReference.expression.getText(), node); - } - } - - protected visitImportDeclaration(node: ts.ImportDeclaration): void { - this._validateImport(node.moduleSpecifier.getText(), node); - } - - protected visitCallExpression(node: ts.CallExpression): void { - super.visitCallExpression(node); - - // import('foo') statements inside the code - if (node.expression.kind === ts.SyntaxKind.ImportKeyword) { - const [path] = node.arguments; - this._validateImport(path.getText(), node); - } - } - - private _validateImport(path: string, node: ts.Node): void { - // remove quotes - path = path.slice(1, -1); - - if (path[0] === '.') { - path = join(dirname(node.getSourceFile().fileName), path); - } - - const parts = dirname(path).split(/\\|\//); - for (let i = parts.length - 1; i >= 0; i--) { - const part = parts[i]; - - if (this._config.allowed.has(part)) { - // GOOD - same layer - return; - } - - if (this._config.disallowed.has(part)) { - // BAD - wrong layer - const message = `Bad layering. You are not allowed to access '${part}' from here, allowed layers are: [${LayeringRule._print(this._config.allowed)}]`; - this.addFailure(this.createFailure(node.getStart(), node.getWidth(), message)); - return; - } - } - } - - static _print(set: Set): string { - const r: string[] = []; - set.forEach(e => r.push(e)); - return r.join(', '); - } -} diff --git a/build/lib/tslint/noDomGlobalsRule.js b/build/lib/tslint/noDomGlobalsRule.js deleted file mode 100644 index 3b992a00e6..0000000000 --- a/build/lib/tslint/noDomGlobalsRule.js +++ /dev/null @@ -1,34 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -const Lint = require("tslint"); -const minimatch = require("minimatch"); -const abstractGlobalsRule_1 = require("./abstractGlobalsRule"); -class Rule extends Lint.Rules.TypedRule { - applyWithProgram(sourceFile, program) { - const configs = this.getOptions().ruleArguments; - for (const config of configs) { - if (minimatch(sourceFile.fileName, config.target)) { - return this.applyWithWalker(new NoDOMGlobalsRuleWalker(sourceFile, program, this.getOptions(), config)); - } - } - return []; - } -} -exports.Rule = Rule; -class NoDOMGlobalsRuleWalker extends abstractGlobalsRule_1.AbstractGlobalsRuleWalker { - getDefinitionPattern() { - return 'lib.dom.d.ts'; - } - getDisallowedGlobals() { - // intentionally not complete - return [ - "window", - "document", - "HTMLElement" - ]; - } -} diff --git a/build/lib/tslint/noDomGlobalsRule.ts b/build/lib/tslint/noDomGlobalsRule.ts deleted file mode 100644 index f3c493f228..0000000000 --- a/build/lib/tslint/noDomGlobalsRule.ts +++ /dev/null @@ -1,45 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as ts from 'typescript'; -import * as Lint from 'tslint'; -import * as minimatch from 'minimatch'; -import { AbstractGlobalsRuleWalker } from './abstractGlobalsRule'; - -interface NoDOMGlobalsRuleConfig { - target: string; - allowed: string[]; -} - -export class Rule extends Lint.Rules.TypedRule { - - applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] { - const configs = this.getOptions().ruleArguments; - - for (const config of configs) { - if (minimatch(sourceFile.fileName, config.target)) { - return this.applyWithWalker(new NoDOMGlobalsRuleWalker(sourceFile, program, this.getOptions(), config)); - } - } - - return []; - } -} - -class NoDOMGlobalsRuleWalker extends AbstractGlobalsRuleWalker { - - getDefinitionPattern(): string { - return 'lib.dom.d.ts'; - } - - getDisallowedGlobals(): string[] { - // intentionally not complete - return [ - "window", - "document", - "HTMLElement" - ]; - } -} diff --git a/build/lib/tslint/noNewBufferRule.js b/build/lib/tslint/noNewBufferRule.js deleted file mode 100644 index c4f219a595..0000000000 --- a/build/lib/tslint/noNewBufferRule.js +++ /dev/null @@ -1,22 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -const ts = require("typescript"); -const Lint = require("tslint"); -class Rule extends Lint.Rules.AbstractRule { - apply(sourceFile) { - return this.applyWithWalker(new NewBufferRuleWalker(sourceFile, this.getOptions())); - } -} -exports.Rule = Rule; -class NewBufferRuleWalker extends Lint.RuleWalker { - visitNewExpression(node) { - if (node.expression.kind === ts.SyntaxKind.Identifier && node.expression && node.expression.text === 'Buffer') { - this.addFailureAtNode(node, '`new Buffer` is deprecated. Consider Buffer.From or Buffer.alloc instead.'); - } - super.visitNewExpression(node); - } -} diff --git a/build/lib/tslint/noNewBufferRule.ts b/build/lib/tslint/noNewBufferRule.ts deleted file mode 100644 index 7617d32033..0000000000 --- a/build/lib/tslint/noNewBufferRule.ts +++ /dev/null @@ -1,23 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as ts from 'typescript'; -import * as Lint from 'tslint'; - -export class Rule extends Lint.Rules.AbstractRule { - apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - return this.applyWithWalker(new NewBufferRuleWalker(sourceFile, this.getOptions())); - } -} - -class NewBufferRuleWalker extends Lint.RuleWalker { - visitNewExpression(node: ts.NewExpression) { - if (node.expression.kind === ts.SyntaxKind.Identifier && node.expression && (node.expression as ts.Identifier).text === 'Buffer') { - this.addFailureAtNode(node, '`new Buffer` is deprecated. Consider Buffer.From or Buffer.alloc instead.'); - } - - super.visitNewExpression(node); - } -} \ No newline at end of file diff --git a/build/lib/tslint/noNlsInStandaloneEditorRule.js b/build/lib/tslint/noNlsInStandaloneEditorRule.js deleted file mode 100644 index 28528d2e3e..0000000000 --- a/build/lib/tslint/noNlsInStandaloneEditorRule.js +++ /dev/null @@ -1,54 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -const ts = require("typescript"); -const Lint = require("tslint"); -const path_1 = require("path"); -class Rule extends Lint.Rules.AbstractRule { - apply(sourceFile) { - if (/vs(\/|\\)editor(\/|\\)standalone(\/|\\)/.test(sourceFile.fileName) - || /vs(\/|\\)editor(\/|\\)common(\/|\\)standalone(\/|\\)/.test(sourceFile.fileName) - || /vs(\/|\\)editor(\/|\\)editor.api/.test(sourceFile.fileName) - || /vs(\/|\\)editor(\/|\\)editor.main/.test(sourceFile.fileName) - || /vs(\/|\\)editor(\/|\\)editor.worker/.test(sourceFile.fileName)) { - return this.applyWithWalker(new NoNlsInStandaloneEditorRuleWalker(sourceFile, this.getOptions())); - } - return []; - } -} -exports.Rule = Rule; -class NoNlsInStandaloneEditorRuleWalker extends Lint.RuleWalker { - constructor(file, opts) { - super(file, opts); - } - visitImportEqualsDeclaration(node) { - if (node.moduleReference.kind === ts.SyntaxKind.ExternalModuleReference) { - this._validateImport(node.moduleReference.expression.getText(), node); - } - } - visitImportDeclaration(node) { - this._validateImport(node.moduleSpecifier.getText(), node); - } - visitCallExpression(node) { - super.visitCallExpression(node); - // import('foo') statements inside the code - if (node.expression.kind === ts.SyntaxKind.ImportKeyword) { - const [path] = node.arguments; - this._validateImport(path.getText(), node); - } - } - _validateImport(path, node) { - // remove quotes - path = path.slice(1, -1); - // resolve relative paths - if (path[0] === '.') { - path = path_1.join(this.getSourceFile().fileName, path); - } - if (/vs(\/|\\)nls/.test(path)) { - this.addFailure(this.createFailure(node.getStart(), node.getWidth(), `Not allowed to import vs/nls in standalone editor modules. Use standaloneStrings.ts`)); - } - } -} diff --git a/build/lib/tslint/noNlsInStandaloneEditorRule.ts b/build/lib/tslint/noNlsInStandaloneEditorRule.ts deleted file mode 100644 index 5fb7abac88..0000000000 --- a/build/lib/tslint/noNlsInStandaloneEditorRule.ts +++ /dev/null @@ -1,67 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as ts from 'typescript'; -import * as Lint from 'tslint'; -import { join } from 'path'; - -export class Rule extends Lint.Rules.AbstractRule { - public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - if ( - /vs(\/|\\)editor(\/|\\)standalone(\/|\\)/.test(sourceFile.fileName) - || /vs(\/|\\)editor(\/|\\)common(\/|\\)standalone(\/|\\)/.test(sourceFile.fileName) - || /vs(\/|\\)editor(\/|\\)editor.api/.test(sourceFile.fileName) - || /vs(\/|\\)editor(\/|\\)editor.main/.test(sourceFile.fileName) - || /vs(\/|\\)editor(\/|\\)editor.worker/.test(sourceFile.fileName) - ) { - return this.applyWithWalker(new NoNlsInStandaloneEditorRuleWalker(sourceFile, this.getOptions())); - } - - return []; - } -} - -class NoNlsInStandaloneEditorRuleWalker extends Lint.RuleWalker { - - constructor(file: ts.SourceFile, opts: Lint.IOptions) { - super(file, opts); - } - - protected visitImportEqualsDeclaration(node: ts.ImportEqualsDeclaration): void { - if (node.moduleReference.kind === ts.SyntaxKind.ExternalModuleReference) { - this._validateImport(node.moduleReference.expression.getText(), node); - } - } - - protected visitImportDeclaration(node: ts.ImportDeclaration): void { - this._validateImport(node.moduleSpecifier.getText(), node); - } - - protected visitCallExpression(node: ts.CallExpression): void { - super.visitCallExpression(node); - - // import('foo') statements inside the code - if (node.expression.kind === ts.SyntaxKind.ImportKeyword) { - const [path] = node.arguments; - this._validateImport(path.getText(), node); - } - } - - private _validateImport(path: string, node: ts.Node): void { - // remove quotes - path = path.slice(1, -1); - - // resolve relative paths - if (path[0] === '.') { - path = join(this.getSourceFile().fileName, path); - } - - if ( - /vs(\/|\\)nls/.test(path) - ) { - this.addFailure(this.createFailure(node.getStart(), node.getWidth(), `Not allowed to import vs/nls in standalone editor modules. Use standaloneStrings.ts`)); - } - } -} diff --git a/build/lib/tslint/noNodejsGlobalsRule.js b/build/lib/tslint/noNodejsGlobalsRule.js deleted file mode 100644 index 7189d8f21d..0000000000 --- a/build/lib/tslint/noNodejsGlobalsRule.js +++ /dev/null @@ -1,41 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -const Lint = require("tslint"); -const minimatch = require("minimatch"); -const abstractGlobalsRule_1 = require("./abstractGlobalsRule"); -class Rule extends Lint.Rules.TypedRule { - applyWithProgram(sourceFile, program) { - const configs = this.getOptions().ruleArguments; - for (const config of configs) { - if (minimatch(sourceFile.fileName, config.target)) { - return this.applyWithWalker(new NoNodejsGlobalsRuleWalker(sourceFile, program, this.getOptions(), config)); - } - } - return []; - } -} -exports.Rule = Rule; -class NoNodejsGlobalsRuleWalker extends abstractGlobalsRule_1.AbstractGlobalsRuleWalker { - getDefinitionPattern() { - return '@types/node'; - } - getDisallowedGlobals() { - // https://nodejs.org/api/globals.html#globals_global_objects - return [ - "NodeJS", - "Buffer", - "__dirname", - "__filename", - "clearImmediate", - "exports", - "global", - "module", - "process", - "setImmediate" - ]; - } -} diff --git a/build/lib/tslint/noNodejsGlobalsRule.ts b/build/lib/tslint/noNodejsGlobalsRule.ts deleted file mode 100644 index 0ac0e74511..0000000000 --- a/build/lib/tslint/noNodejsGlobalsRule.ts +++ /dev/null @@ -1,52 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as ts from 'typescript'; -import * as Lint from 'tslint'; -import * as minimatch from 'minimatch'; -import { AbstractGlobalsRuleWalker } from './abstractGlobalsRule'; - -interface NoNodejsGlobalsConfig { - target: string; - allowed: string[]; -} - -export class Rule extends Lint.Rules.TypedRule { - - applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] { - const configs = this.getOptions().ruleArguments; - - for (const config of configs) { - if (minimatch(sourceFile.fileName, config.target)) { - return this.applyWithWalker(new NoNodejsGlobalsRuleWalker(sourceFile, program, this.getOptions(), config)); - } - } - - return []; - } -} - -class NoNodejsGlobalsRuleWalker extends AbstractGlobalsRuleWalker { - - getDefinitionPattern(): string { - return '@types/node'; - } - - getDisallowedGlobals(): string[] { - // https://nodejs.org/api/globals.html#globals_global_objects - return [ - "NodeJS", - "Buffer", - "__dirname", - "__filename", - "clearImmediate", - "exports", - "global", - "module", - "process", - "setImmediate" - ]; - } -} diff --git a/build/lib/tslint/noStandaloneEditorRule.js b/build/lib/tslint/noStandaloneEditorRule.js deleted file mode 100644 index c1f2ed51e6..0000000000 --- a/build/lib/tslint/noStandaloneEditorRule.js +++ /dev/null @@ -1,57 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -const ts = require("typescript"); -const Lint = require("tslint"); -const path_1 = require("path"); -class Rule extends Lint.Rules.AbstractRule { - apply(sourceFile) { - if (/vs(\/|\\)editor/.test(sourceFile.fileName)) { - // the vs/editor folder is allowed to use the standalone editor - return []; - } - return this.applyWithWalker(new NoStandaloneEditorRuleWalker(sourceFile, this.getOptions())); - } -} -exports.Rule = Rule; -class NoStandaloneEditorRuleWalker extends Lint.RuleWalker { - constructor(file, opts) { - super(file, opts); - } - visitImportEqualsDeclaration(node) { - if (node.moduleReference.kind === ts.SyntaxKind.ExternalModuleReference) { - this._validateImport(node.moduleReference.expression.getText(), node); - } - } - visitImportDeclaration(node) { - this._validateImport(node.moduleSpecifier.getText(), node); - } - visitCallExpression(node) { - super.visitCallExpression(node); - // import('foo') statements inside the code - if (node.expression.kind === ts.SyntaxKind.ImportKeyword) { - const [path] = node.arguments; - this._validateImport(path.getText(), node); - } - } - // {{SQL CARBON EDIT}} - Rename node argument to _node to prevent errors since it is not used - _validateImport(path, _node) { - // remove quotes - path = path.slice(1, -1); - // resolve relative paths - if (path[0] === '.') { - path = path_1.join(this.getSourceFile().fileName, path); - } - if (/vs(\/|\\)editor(\/|\\)standalone(\/|\\)/.test(path) - || /vs(\/|\\)editor(\/|\\)common(\/|\\)standalone(\/|\\)/.test(path) - || /vs(\/|\\)editor(\/|\\)editor.api/.test(path) - || /vs(\/|\\)editor(\/|\\)editor.main/.test(path) - || /vs(\/|\\)editor(\/|\\)editor.worker/.test(path)) { - // {{SQL CARBON EDIT}} - //this.addFailure(this.createFailure(node.getStart(), node.getWidth(), `Not allowed to import standalone editor modules. See https://github.com/Microsoft/vscode/wiki/Code-Organization`)); - } - } -} diff --git a/build/lib/tslint/noStandaloneEditorRule.ts b/build/lib/tslint/noStandaloneEditorRule.ts deleted file mode 100644 index a29e9b18ab..0000000000 --- a/build/lib/tslint/noStandaloneEditorRule.ts +++ /dev/null @@ -1,67 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as ts from 'typescript'; -import * as Lint from 'tslint'; -import { join } from 'path'; - -export class Rule extends Lint.Rules.AbstractRule { - public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - if (/vs(\/|\\)editor/.test(sourceFile.fileName)) { - // the vs/editor folder is allowed to use the standalone editor - return []; - } - return this.applyWithWalker(new NoStandaloneEditorRuleWalker(sourceFile, this.getOptions())); - } -} - -class NoStandaloneEditorRuleWalker extends Lint.RuleWalker { - - constructor(file: ts.SourceFile, opts: Lint.IOptions) { - super(file, opts); - } - - protected visitImportEqualsDeclaration(node: ts.ImportEqualsDeclaration): void { - if (node.moduleReference.kind === ts.SyntaxKind.ExternalModuleReference) { - this._validateImport(node.moduleReference.expression.getText(), node); - } - } - - protected visitImportDeclaration(node: ts.ImportDeclaration): void { - this._validateImport(node.moduleSpecifier.getText(), node); - } - - protected visitCallExpression(node: ts.CallExpression): void { - super.visitCallExpression(node); - - // import('foo') statements inside the code - if (node.expression.kind === ts.SyntaxKind.ImportKeyword) { - const [path] = node.arguments; - this._validateImport(path.getText(), node); - } - } - - // {{SQL CARBON EDIT}} - Rename node argument to _node to prevent errors since it is not used - private _validateImport(path: string, _node: ts.Node): void { - // remove quotes - path = path.slice(1, -1); - - // resolve relative paths - if (path[0] === '.') { - path = join(this.getSourceFile().fileName, path); - } - - if ( - /vs(\/|\\)editor(\/|\\)standalone(\/|\\)/.test(path) - || /vs(\/|\\)editor(\/|\\)common(\/|\\)standalone(\/|\\)/.test(path) - || /vs(\/|\\)editor(\/|\\)editor.api/.test(path) - || /vs(\/|\\)editor(\/|\\)editor.main/.test(path) - || /vs(\/|\\)editor(\/|\\)editor.worker/.test(path) - ) { - // {{SQL CARBON EDIT}} - //this.addFailure(this.createFailure(node.getStart(), node.getWidth(), `Not allowed to import standalone editor modules. See https://github.com/Microsoft/vscode/wiki/Code-Organization`)); - } - } -} diff --git a/build/lib/tslint/noSyncRule.js b/build/lib/tslint/noSyncRule.js deleted file mode 100644 index f4d92f5a59..0000000000 --- a/build/lib/tslint/noSyncRule.js +++ /dev/null @@ -1,33 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -const Lint = require("tslint"); -const minimatch = require("minimatch"); -class Rule extends Lint.Rules.AbstractRule { - apply(sourceFile) { - const args = this.getOptions().ruleArguments[0]; - if (args.exclude.every(x => !minimatch(sourceFile.fileName, x))) { - return this.applyWithWalker(new NoSyncRuleWalker(sourceFile, this.getOptions())); - } - return []; - } -} -exports.Rule = Rule; -class NoSyncRuleWalker extends Lint.RuleWalker { - constructor(file, opts) { - super(file, opts); - } - visitCallExpression(node) { - if (node.expression && NoSyncRuleWalker.operations.some(x => node.expression.getText().indexOf(x) >= 0)) { - this.addFailureAtNode(node, `Do not use Sync operations`); - } - super.visitCallExpression(node); - } -} -NoSyncRuleWalker.operations = ['readFileSync', 'writeFileSync', 'existsSync', 'fchmodSync', 'lchmodSync', - 'statSync', 'fstatSync', 'lstatSync', 'linkSync', 'symlinkSync', 'readlinkSync', 'realpathSync', 'unlinkSync', 'rmdirSync', - 'mkdirSync', 'mkdtempSync', 'readdirSync', 'openSync', 'utimesSync', 'futimesSync', 'fsyncSync', 'writeSync', 'readSync', - 'appendFileSync', 'accessSync', 'fdatasyncSync', 'copyFileSync']; diff --git a/build/lib/tslint/noSyncRule.ts b/build/lib/tslint/noSyncRule.ts deleted file mode 100644 index 9d0254209d..0000000000 --- a/build/lib/tslint/noSyncRule.ts +++ /dev/null @@ -1,45 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as ts from 'typescript'; -import * as Lint from 'tslint'; -import * as minimatch from 'minimatch'; - -interface NoSyncRuleConfig { - exclude: string[]; -} - -export class Rule extends Lint.Rules.AbstractRule { - - apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - const args = this.getOptions().ruleArguments[0]; - - if (args.exclude.every(x => !minimatch(sourceFile.fileName, x))) { - return this.applyWithWalker(new NoSyncRuleWalker(sourceFile, this.getOptions())); - } - - return []; - } -} - -class NoSyncRuleWalker extends Lint.RuleWalker { - - private static readonly operations = ['readFileSync', 'writeFileSync', 'existsSync', 'fchmodSync', 'lchmodSync', - 'statSync', 'fstatSync', 'lstatSync', 'linkSync', 'symlinkSync', 'readlinkSync', 'realpathSync', 'unlinkSync', 'rmdirSync', - 'mkdirSync', 'mkdtempSync', 'readdirSync', 'openSync', 'utimesSync', 'futimesSync', 'fsyncSync', 'writeSync', 'readSync', - 'appendFileSync', 'accessSync', 'fdatasyncSync', 'copyFileSync']; - - constructor(file: ts.SourceFile, opts: Lint.IOptions) { - super(file, opts); - } - - visitCallExpression(node: ts.CallExpression) { - if (node.expression && NoSyncRuleWalker.operations.some(x => node.expression.getText().indexOf(x) >= 0)) { - this.addFailureAtNode(node, `Do not use Sync operations`); - } - - super.visitCallExpression(node); - } -} diff --git a/build/lib/tslint/noUnderscoreInLocalizeKeysRule.js b/build/lib/tslint/noUnderscoreInLocalizeKeysRule.js deleted file mode 100644 index 32a010048c..0000000000 --- a/build/lib/tslint/noUnderscoreInLocalizeKeysRule.js +++ /dev/null @@ -1,49 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -const ts = require("typescript"); -const Lint = require("tslint"); -/** - * Implementation of the no-localize-keys-with-underscore rule which verifies that keys to the localize - * calls don't contain underscores (_) since those break the localization process. - */ -class Rule extends Lint.Rules.AbstractRule { - apply(sourceFile) { - return this.applyWithWalker(new NoLocalizeKeysWithUnderscore(sourceFile, this.getOptions())); - } -} -exports.Rule = Rule; -const signatures = [ - "localize", - "nls.localize" -]; -class NoLocalizeKeysWithUnderscore extends Lint.RuleWalker { - constructor(file, opts) { - super(file, opts); - } - visitCallExpression(node) { - this.checkCallExpression(node); - super.visitCallExpression(node); - } - checkCallExpression(node) { - // If this isn't one of the localize functions then continue on - const functionName = node.expression.getText(); - if (functionName && !signatures.some(s => s === functionName)) { - return; - } - const arg = node && node.arguments && node.arguments.length > 0 ? node.arguments[0] : undefined; // The key is the first element - // Ignore if the arg isn't a string - we expect the compiler to warn if that's an issue - if (arg && ts.isStringLiteral(arg)) { - if (arg.getText().indexOf('_') >= 0) { - const fix = [ - Lint.Replacement.replaceFromTo(arg.getStart(), arg.getEnd(), `${arg.getText().replace(/_/g, '.')}`), - ]; - this.addFailure(this.createFailure(arg.getStart(), arg.getWidth(), `Keys for localize calls must not contain underscores. Use periods (.) instead.`, fix)); - return; - } - } - } -} diff --git a/build/lib/tslint/noUnderscoreInLocalizeKeysRule.ts b/build/lib/tslint/noUnderscoreInLocalizeKeysRule.ts deleted file mode 100644 index 944a499b56..0000000000 --- a/build/lib/tslint/noUnderscoreInLocalizeKeysRule.ts +++ /dev/null @@ -1,55 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as ts from 'typescript'; -import * as Lint from 'tslint'; - -/** - * Implementation of the no-localize-keys-with-underscore rule which verifies that keys to the localize - * calls don't contain underscores (_) since those break the localization process. - */ -export class Rule extends Lint.Rules.AbstractRule { - public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - return this.applyWithWalker(new NoLocalizeKeysWithUnderscore(sourceFile, this.getOptions())); - } -} - -const signatures = [ - "localize", - "nls.localize" -]; - -class NoLocalizeKeysWithUnderscore extends Lint.RuleWalker { - - constructor(file: ts.SourceFile, opts: Lint.IOptions) { - super(file, opts); - } - - protected visitCallExpression(node: ts.CallExpression): void { - this.checkCallExpression(node); - super.visitCallExpression(node); - } - - private checkCallExpression(node: ts.CallExpression): void { - // If this isn't one of the localize functions then continue on - const functionName = node.expression.getText(); - if (functionName && !signatures.some(s => s === functionName)) { - return; - } - const arg = node && node.arguments && node.arguments.length > 0 ? node.arguments[0] : undefined; // The key is the first element - // Ignore if the arg isn't a string - we expect the compiler to warn if that's an issue - if(arg && ts.isStringLiteral(arg)) { - if (arg.getText().indexOf('_') >= 0) { - const fix = [ - Lint.Replacement.replaceFromTo(arg.getStart(), arg.getEnd(), `${arg.getText().replace(/_/g, '.')}`), - ]; - this.addFailure(this.createFailure( - arg.getStart(), arg.getWidth(), - `Keys for localize calls must not contain underscores. Use periods (.) instead.`, fix)); - return; - } - } - } -} diff --git a/build/lib/tslint/noUnexternalizedStringsRule.js b/build/lib/tslint/noUnexternalizedStringsRule.js deleted file mode 100644 index 0c01f5edcb..0000000000 --- a/build/lib/tslint/noUnexternalizedStringsRule.js +++ /dev/null @@ -1,183 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -const ts = require("typescript"); -const Lint = require("tslint"); -/** - * Implementation of the no-unexternalized-strings rule. - */ -class Rule extends Lint.Rules.AbstractRule { - apply(sourceFile) { - if (/\.d.ts$/.test(sourceFile.fileName)) { - return []; - } - return this.applyWithWalker(new NoUnexternalizedStringsRuleWalker(sourceFile, this.getOptions())); - } -} -exports.Rule = Rule; -function isStringLiteral(node) { - return node && node.kind === ts.SyntaxKind.StringLiteral; -} -function isObjectLiteral(node) { - return node && node.kind === ts.SyntaxKind.ObjectLiteralExpression; -} -function isPropertyAssignment(node) { - return node && node.kind === ts.SyntaxKind.PropertyAssignment; -} -class NoUnexternalizedStringsRuleWalker extends Lint.RuleWalker { - constructor(file, opts) { - super(file, opts); - this.signatures = Object.create(null); - this.ignores = Object.create(null); - this.messageIndex = undefined; - this.keyIndex = undefined; - this.usedKeys = Object.create(null); - const options = this.getOptions(); - const first = options && options.length > 0 ? options[0] : null; - if (first) { - if (Array.isArray(first.signatures)) { - first.signatures.forEach((signature) => this.signatures[signature] = true); - } - if (Array.isArray(first.ignores)) { - first.ignores.forEach((ignore) => this.ignores[ignore] = true); - } - if (typeof first.messageIndex !== 'undefined') { - this.messageIndex = first.messageIndex; - } - if (typeof first.keyIndex !== 'undefined') { - this.keyIndex = first.keyIndex; - } - } - } - visitSourceFile(node) { - super.visitSourceFile(node); - Object.keys(this.usedKeys).forEach(key => { - // Keys are quoted. - let identifier = key.substr(1, key.length - 2); - if (!NoUnexternalizedStringsRuleWalker.IDENTIFIER.test(identifier)) { - let occurrence = this.usedKeys[key][0]; - this.addFailure(this.createFailure(occurrence.key.getStart(), occurrence.key.getWidth(), `The key ${occurrence.key.getText()} doesn't conform to a valid localize identifier`)); - } - const occurrences = this.usedKeys[key]; - if (occurrences.length > 1) { - occurrences.forEach(occurrence => { - this.addFailure((this.createFailure(occurrence.key.getStart(), occurrence.key.getWidth(), `Duplicate key ${occurrence.key.getText()} with different message value.`))); - }); - } - }); - } - visitStringLiteral(node) { - this.checkStringLiteral(node); - super.visitStringLiteral(node); - } - checkStringLiteral(node) { - const text = node.getText(); - const doubleQuoted = text.length >= 2 && text[0] === NoUnexternalizedStringsRuleWalker.DOUBLE_QUOTE && text[text.length - 1] === NoUnexternalizedStringsRuleWalker.DOUBLE_QUOTE; - const info = this.findDescribingParent(node); - // Ignore strings in import and export nodes. - if (info && info.isImport && doubleQuoted) { - const fix = [ - Lint.Replacement.replaceFromTo(node.getStart(), 1, '\''), - Lint.Replacement.replaceFromTo(node.getStart() + text.length - 1, 1, '\''), - ]; - this.addFailureAtNode(node, NoUnexternalizedStringsRuleWalker.ImportFailureMessage, fix); - return; - } - const callInfo = info ? info.callInfo : null; - const functionName = callInfo ? callInfo.callExpression.expression.getText() : null; - if (functionName && this.ignores[functionName]) { - return; - } - if (doubleQuoted && (!callInfo || callInfo.argIndex === -1 || !this.signatures[functionName])) { - const s = node.getText(); - const fix = [ - Lint.Replacement.replaceFromTo(node.getStart(), node.getWidth(), `nls.localize('KEY-${s.substring(1, s.length - 1)}', ${s})`), - ]; - this.addFailure(this.createFailure(node.getStart(), node.getWidth(), `Unexternalized string found: ${node.getText()}`, fix)); - return; - } - // We have a single quoted string outside a localize function name. - if (!doubleQuoted && !this.signatures[functionName]) { - return; - } - // We have a string that is a direct argument into the localize call. - const keyArg = callInfo && callInfo.argIndex === this.keyIndex - ? callInfo.callExpression.arguments[this.keyIndex] - : null; - if (keyArg) { - if (isStringLiteral(keyArg)) { - this.recordKey(keyArg, this.messageIndex && callInfo ? callInfo.callExpression.arguments[this.messageIndex] : undefined); - } - else if (isObjectLiteral(keyArg)) { - for (const property of keyArg.properties) { - if (isPropertyAssignment(property)) { - const name = property.name.getText(); - if (name === 'key') { - const initializer = property.initializer; - if (isStringLiteral(initializer)) { - this.recordKey(initializer, this.messageIndex && callInfo ? callInfo.callExpression.arguments[this.messageIndex] : undefined); - } - break; - } - } - } - } - } - const messageArg = callInfo.callExpression.arguments[this.messageIndex]; - if (messageArg && messageArg.kind !== ts.SyntaxKind.StringLiteral) { - this.addFailure(this.createFailure(messageArg.getStart(), messageArg.getWidth(), `Message argument to '${callInfo.callExpression.expression.getText()}' must be a string literal.`)); - return; - } - } - recordKey(keyNode, messageNode) { - const text = keyNode.getText(); - // We have an empty key - if (text.match(/(['"]) *\1/)) { - if (messageNode) { - this.addFailureAtNode(keyNode, `Key is empty for message: ${messageNode.getText()}`); - } - else { - this.addFailureAtNode(keyNode, `Key is empty.`); - } - return; - } - let occurrences = this.usedKeys[text]; - if (!occurrences) { - occurrences = []; - this.usedKeys[text] = occurrences; - } - if (messageNode) { - if (occurrences.some(pair => pair.message ? pair.message.getText() === messageNode.getText() : false)) { - return; - } - } - occurrences.push({ key: keyNode, message: messageNode }); - } - findDescribingParent(node) { - let parent; - while ((parent = node.parent)) { - const kind = parent.kind; - if (kind === ts.SyntaxKind.CallExpression) { - const callExpression = parent; - return { callInfo: { callExpression: callExpression, argIndex: callExpression.arguments.indexOf(node) } }; - } - else if (kind === ts.SyntaxKind.ImportEqualsDeclaration || kind === ts.SyntaxKind.ImportDeclaration || kind === ts.SyntaxKind.ExportDeclaration) { - return { isImport: true }; - } - else if (kind === ts.SyntaxKind.VariableDeclaration || kind === ts.SyntaxKind.FunctionDeclaration || kind === ts.SyntaxKind.PropertyDeclaration - || kind === ts.SyntaxKind.MethodDeclaration || kind === ts.SyntaxKind.VariableDeclarationList || kind === ts.SyntaxKind.InterfaceDeclaration - || kind === ts.SyntaxKind.ClassDeclaration || kind === ts.SyntaxKind.EnumDeclaration || kind === ts.SyntaxKind.ModuleDeclaration - || kind === ts.SyntaxKind.TypeAliasDeclaration || kind === ts.SyntaxKind.SourceFile) { - return null; - } - node = parent; - } - return null; - } -} -NoUnexternalizedStringsRuleWalker.ImportFailureMessage = 'Do not use double quotes for imports.'; -NoUnexternalizedStringsRuleWalker.DOUBLE_QUOTE = '"'; -NoUnexternalizedStringsRuleWalker.IDENTIFIER = /^[_a-zA-Z0-9][ .\-_a-zA-Z0-9]*$/; diff --git a/build/lib/tslint/noUnexternalizedStringsRule.ts b/build/lib/tslint/noUnexternalizedStringsRule.ts deleted file mode 100644 index 0b8c2e4bf0..0000000000 --- a/build/lib/tslint/noUnexternalizedStringsRule.ts +++ /dev/null @@ -1,222 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as ts from 'typescript'; -import * as Lint from 'tslint'; - -/** - * Implementation of the no-unexternalized-strings rule. - */ -export class Rule extends Lint.Rules.AbstractRule { - public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - if (/\.d.ts$/.test(sourceFile.fileName)) { - return []; - } - return this.applyWithWalker(new NoUnexternalizedStringsRuleWalker(sourceFile, this.getOptions())); - } -} - -interface Map { - [key: string]: V; -} - -interface UnexternalizedStringsOptions { - signatures?: string[]; - messageIndex?: number; - keyIndex?: number; - ignores?: string[]; -} - -function isStringLiteral(node: ts.Node): node is ts.StringLiteral { - return node && node.kind === ts.SyntaxKind.StringLiteral; -} - -function isObjectLiteral(node: ts.Node): node is ts.ObjectLiteralExpression { - return node && node.kind === ts.SyntaxKind.ObjectLiteralExpression; -} - -function isPropertyAssignment(node: ts.Node): node is ts.PropertyAssignment { - return node && node.kind === ts.SyntaxKind.PropertyAssignment; -} - -interface KeyMessagePair { - key: ts.StringLiteral; - message: ts.Node | undefined; -} - -class NoUnexternalizedStringsRuleWalker extends Lint.RuleWalker { - - private static ImportFailureMessage = 'Do not use double quotes for imports.'; - - private static DOUBLE_QUOTE: string = '"'; - - private signatures: Map; - private messageIndex: number | undefined; - private keyIndex: number | undefined; - private ignores: Map; - - private usedKeys: Map; - - constructor(file: ts.SourceFile, opts: Lint.IOptions) { - super(file, opts); - this.signatures = Object.create(null); - this.ignores = Object.create(null); - this.messageIndex = undefined; - this.keyIndex = undefined; - this.usedKeys = Object.create(null); - const options: any[] = this.getOptions(); - const first: UnexternalizedStringsOptions = options && options.length > 0 ? options[0] : null; - if (first) { - if (Array.isArray(first.signatures)) { - first.signatures.forEach((signature: string) => this.signatures[signature] = true); - } - if (Array.isArray(first.ignores)) { - first.ignores.forEach((ignore: string) => this.ignores[ignore] = true); - } - if (typeof first.messageIndex !== 'undefined') { - this.messageIndex = first.messageIndex; - } - if (typeof first.keyIndex !== 'undefined') { - this.keyIndex = first.keyIndex; - } - } - } - - private static IDENTIFIER = /^[_a-zA-Z0-9][ .\-_a-zA-Z0-9]*$/; - protected visitSourceFile(node: ts.SourceFile): void { - super.visitSourceFile(node); - Object.keys(this.usedKeys).forEach(key => { - // Keys are quoted. - let identifier = key.substr(1, key.length - 2); - if (!NoUnexternalizedStringsRuleWalker.IDENTIFIER.test(identifier)) { - let occurrence = this.usedKeys[key][0]; - this.addFailure(this.createFailure(occurrence.key.getStart(), occurrence.key.getWidth(), `The key ${occurrence.key.getText()} doesn't conform to a valid localize identifier`)); - } - const occurrences = this.usedKeys[key]; - if (occurrences.length > 1) { - occurrences.forEach(occurrence => { - this.addFailure((this.createFailure(occurrence.key.getStart(), occurrence.key.getWidth(), `Duplicate key ${occurrence.key.getText()} with different message value.`))); - }); - } - }); - } - - protected visitStringLiteral(node: ts.StringLiteral): void { - this.checkStringLiteral(node); - super.visitStringLiteral(node); - } - - private checkStringLiteral(node: ts.StringLiteral): void { - const text = node.getText(); - const doubleQuoted = text.length >= 2 && text[0] === NoUnexternalizedStringsRuleWalker.DOUBLE_QUOTE && text[text.length - 1] === NoUnexternalizedStringsRuleWalker.DOUBLE_QUOTE; - const info = this.findDescribingParent(node); - // Ignore strings in import and export nodes. - if (info && info.isImport && doubleQuoted) { - const fix = [ - Lint.Replacement.replaceFromTo(node.getStart(), 1, '\''), - Lint.Replacement.replaceFromTo(node.getStart() + text.length - 1, 1, '\''), - ]; - this.addFailureAtNode( - node, - NoUnexternalizedStringsRuleWalker.ImportFailureMessage, - fix - ); - return; - } - const callInfo = info ? info.callInfo : null; - const functionName = callInfo ? callInfo.callExpression.expression.getText() : null; - if (functionName && this.ignores[functionName]) { - return; - } - - if (doubleQuoted && (!callInfo || callInfo.argIndex === -1 || !this.signatures[functionName!])) { - const s = node.getText(); - const fix = [ - Lint.Replacement.replaceFromTo(node.getStart(), node.getWidth(), `nls.localize('KEY-${s.substring(1, s.length - 1)}', ${s})`), - ]; - this.addFailure(this.createFailure(node.getStart(), node.getWidth(), `Unexternalized string found: ${node.getText()}`, fix)); - return; - } - // We have a single quoted string outside a localize function name. - if (!doubleQuoted && !this.signatures[functionName!]) { - return; - } - // We have a string that is a direct argument into the localize call. - const keyArg: ts.Expression | null = callInfo && callInfo.argIndex === this.keyIndex - ? callInfo.callExpression.arguments[this.keyIndex] - : null; - if (keyArg) { - if (isStringLiteral(keyArg)) { - this.recordKey(keyArg, this.messageIndex && callInfo ? callInfo.callExpression.arguments[this.messageIndex] : undefined); - } else if (isObjectLiteral(keyArg)) { - for (const property of keyArg.properties) { - if (isPropertyAssignment(property)) { - const name = property.name.getText(); - if (name === 'key') { - const initializer = property.initializer; - if (isStringLiteral(initializer)) { - this.recordKey(initializer, this.messageIndex && callInfo ? callInfo.callExpression.arguments[this.messageIndex] : undefined); - } - break; - } - } - } - } - } - - const messageArg = callInfo!.callExpression.arguments[this.messageIndex!]; - - if (messageArg && messageArg.kind !== ts.SyntaxKind.StringLiteral) { - this.addFailure(this.createFailure( - messageArg.getStart(), messageArg.getWidth(), - `Message argument to '${callInfo!.callExpression.expression.getText()}' must be a string literal.`)); - return; - } - } - - private recordKey(keyNode: ts.StringLiteral, messageNode: ts.Node | undefined) { - const text = keyNode.getText(); - // We have an empty key - if (text.match(/(['"]) *\1/)) { - if (messageNode) { - this.addFailureAtNode(keyNode, `Key is empty for message: ${messageNode.getText()}`); - } else { - this.addFailureAtNode(keyNode, `Key is empty.`); - } - return; - } - let occurrences: KeyMessagePair[] = this.usedKeys[text]; - if (!occurrences) { - occurrences = []; - this.usedKeys[text] = occurrences; - } - if (messageNode) { - if (occurrences.some(pair => pair.message ? pair.message.getText() === messageNode.getText() : false)) { - return; - } - } - occurrences.push({ key: keyNode, message: messageNode }); - } - - private findDescribingParent(node: ts.Node): { callInfo?: { callExpression: ts.CallExpression, argIndex: number }, isImport?: boolean; } | null { - let parent: ts.Node; - while ((parent = node.parent)) { - const kind = parent.kind; - if (kind === ts.SyntaxKind.CallExpression) { - const callExpression = parent as ts.CallExpression; - return { callInfo: { callExpression: callExpression, argIndex: callExpression.arguments.indexOf(node) } }; - } else if (kind === ts.SyntaxKind.ImportEqualsDeclaration || kind === ts.SyntaxKind.ImportDeclaration || kind === ts.SyntaxKind.ExportDeclaration) { - return { isImport: true }; - } else if (kind === ts.SyntaxKind.VariableDeclaration || kind === ts.SyntaxKind.FunctionDeclaration || kind === ts.SyntaxKind.PropertyDeclaration - || kind === ts.SyntaxKind.MethodDeclaration || kind === ts.SyntaxKind.VariableDeclarationList || kind === ts.SyntaxKind.InterfaceDeclaration - || kind === ts.SyntaxKind.ClassDeclaration || kind === ts.SyntaxKind.EnumDeclaration || kind === ts.SyntaxKind.ModuleDeclaration - || kind === ts.SyntaxKind.TypeAliasDeclaration || kind === ts.SyntaxKind.SourceFile) { - return null; - } - node = parent; - } - return null; - } -} diff --git a/build/lib/tslint/noUselessStrictRule.js b/build/lib/tslint/noUselessStrictRule.js deleted file mode 100644 index 925b64133e..0000000000 --- a/build/lib/tslint/noUselessStrictRule.js +++ /dev/null @@ -1,28 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -const Lint = require("tslint"); -class Rule extends Lint.Rules.TypedRule { - applyWithProgram(sourceFile, program) { - if (program.getCompilerOptions().alwaysStrict) { - return this.applyWithWalker(new NoUselessStrictRuleWalker(sourceFile, this.getOptions())); - } - return []; - } -} -exports.Rule = Rule; -class NoUselessStrictRuleWalker extends Lint.RuleWalker { - visitStringLiteral(node) { - this.checkStringLiteral(node); - super.visitStringLiteral(node); - } - checkStringLiteral(node) { - const text = node.getText(); - if (text === '\'use strict\'' || text === '"use strict"') { - this.addFailureAtNode(node, 'use strict directive is unnecessary'); - } - } -} diff --git a/build/lib/tslint/noUselessStrictRule.ts b/build/lib/tslint/noUselessStrictRule.ts deleted file mode 100644 index 64710178dc..0000000000 --- a/build/lib/tslint/noUselessStrictRule.ts +++ /dev/null @@ -1,30 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as Lint from 'tslint'; -import * as ts from 'typescript'; - -export class Rule extends Lint.Rules.TypedRule { - public applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] { - if (program.getCompilerOptions().alwaysStrict) { - return this.applyWithWalker(new NoUselessStrictRuleWalker(sourceFile, this.getOptions())); - } - return []; - } -} - -class NoUselessStrictRuleWalker extends Lint.RuleWalker { - protected visitStringLiteral(node: ts.StringLiteral): void { - this.checkStringLiteral(node); - super.visitStringLiteral(node); - } - - private checkStringLiteral(node: ts.StringLiteral): void { - const text = node.getText(); - if (text === '\'use strict\'' || text === '"use strict"') { - this.addFailureAtNode(node, 'use strict directive is unnecessary'); - } - } -} diff --git a/build/lib/tslint/translationRemindRule.js b/build/lib/tslint/translationRemindRule.js deleted file mode 100644 index ca7dd1ebea..0000000000 --- a/build/lib/tslint/translationRemindRule.js +++ /dev/null @@ -1,62 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -const Lint = require("tslint"); -const fs = require("fs"); -class Rule extends Lint.Rules.AbstractRule { - apply(sourceFile) { - return this.applyWithWalker(new TranslationRemindRuleWalker(sourceFile, this.getOptions())); - } -} -exports.Rule = Rule; -class TranslationRemindRuleWalker extends Lint.RuleWalker { - constructor(file, opts) { - super(file, opts); - } - visitImportDeclaration(node) { - const declaration = node.moduleSpecifier.getText(); - if (declaration !== `'${TranslationRemindRuleWalker.NLS_MODULE}'`) { - return; - } - this.visitImportLikeDeclaration(node); - } - visitImportEqualsDeclaration(node) { - const reference = node.moduleReference.getText(); - if (reference !== `require('${TranslationRemindRuleWalker.NLS_MODULE}')`) { - return; - } - this.visitImportLikeDeclaration(node); - } - visitImportLikeDeclaration(node) { - const currentFile = node.getSourceFile().fileName; - const matchService = currentFile.match(/vs\/workbench\/services\/\w+/); - const matchPart = currentFile.match(/vs\/workbench\/contrib\/\w+/); - if (!matchService && !matchPart) { - return; - } - const resource = matchService ? matchService[0] : matchPart[0]; - let resourceDefined = false; - let json; - try { - json = fs.readFileSync('./build/lib/i18n.resources.json', 'utf8'); - } - catch (e) { - console.error('[translation-remind rule]: File with resources to pull from Transifex was not found. Aborting translation resource check for newly defined workbench part/service.'); - return; - } - const workbenchResources = JSON.parse(json).workbench; - workbenchResources.forEach((existingResource) => { - if (existingResource.name === resource) { - resourceDefined = true; - return; - } - }); - if (!resourceDefined) { - this.addFailureAtNode(node, `Please add '${resource}' to ./build/lib/i18n.resources.json file to use translations here.`); - } - } -} -TranslationRemindRuleWalker.NLS_MODULE = 'vs/nls'; diff --git a/build/lib/tslint/translationRemindRule.ts b/build/lib/tslint/translationRemindRule.ts deleted file mode 100644 index d5e5050d21..0000000000 --- a/build/lib/tslint/translationRemindRule.ts +++ /dev/null @@ -1,73 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as ts from 'typescript'; -import * as Lint from 'tslint'; -import * as fs from 'fs'; - -export class Rule extends Lint.Rules.AbstractRule { - public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - return this.applyWithWalker(new TranslationRemindRuleWalker(sourceFile, this.getOptions())); - } -} - -class TranslationRemindRuleWalker extends Lint.RuleWalker { - - private static NLS_MODULE: string = 'vs/nls'; - - constructor(file: ts.SourceFile, opts: Lint.IOptions) { - super(file, opts); - } - - protected visitImportDeclaration(node: ts.ImportDeclaration): void { - const declaration = node.moduleSpecifier.getText(); - if (declaration !== `'${TranslationRemindRuleWalker.NLS_MODULE}'`) { - return; - } - - this.visitImportLikeDeclaration(node); - } - - protected visitImportEqualsDeclaration(node: ts.ImportEqualsDeclaration): void { - const reference = node.moduleReference.getText(); - if (reference !== `require('${TranslationRemindRuleWalker.NLS_MODULE}')`) { - return; - } - - this.visitImportLikeDeclaration(node); - } - - private visitImportLikeDeclaration(node: ts.ImportDeclaration | ts.ImportEqualsDeclaration) { - const currentFile = node.getSourceFile().fileName; - const matchService = currentFile.match(/vs\/workbench\/services\/\w+/); - const matchPart = currentFile.match(/vs\/workbench\/contrib\/\w+/); - if (!matchService && !matchPart) { - return; - } - - const resource = matchService ? matchService[0] : matchPart![0]; - let resourceDefined = false; - - let json; - try { - json = fs.readFileSync('./build/lib/i18n.resources.json', 'utf8'); - } catch (e) { - console.error('[translation-remind rule]: File with resources to pull from Transifex was not found. Aborting translation resource check for newly defined workbench part/service.'); - return; - } - const workbenchResources = JSON.parse(json).workbench; - - workbenchResources.forEach((existingResource: any) => { - if (existingResource.name === resource) { - resourceDefined = true; - return; - } - }); - - if (!resourceDefined) { - this.addFailureAtNode(node, `Please add '${resource}' to ./build/lib/i18n.resources.json file to use translations here.`); - } - } -} diff --git a/build/monaco/api.js b/build/monaco/api.js index 8b74d23d62..4b56799bfe 100644 --- a/build/monaco/api.js +++ b/build/monaco/api.js @@ -9,7 +9,7 @@ const ts = require("typescript"); const path = require("path"); const fancyLog = require("fancy-log"); const ansiColors = require("ansi-colors"); -const dtsv = '2'; +const dtsv = '3'; const tsfmt = require('../../tsfmt.json'); const SRC = path.join(__dirname, '../../src'); exports.RECIPE_PATH = path.join(__dirname, './monaco.d.ts.recipe'); @@ -148,12 +148,44 @@ function getMassagedTopLevelDeclarationText(sourceFile, declaration, importName, } }); } + else if (declaration.kind === ts.SyntaxKind.VariableStatement) { + const jsDoc = result.substr(0, declaration.getLeadingTriviaWidth(sourceFile)); + if (jsDoc.indexOf('@monacodtsreplace') >= 0) { + const jsDocLines = jsDoc.split(/\r\n|\r|\n/); + let directives = []; + for (const jsDocLine of jsDocLines) { + const m = jsDocLine.match(/^\s*\* \/([^/]+)\/([^/]+)\/$/); + if (m) { + directives.push([new RegExp(m[1], 'g'), m[2]]); + } + } + // remove the jsdoc + result = result.substr(jsDoc.length); + if (directives.length > 0) { + // apply replace directives + const replacer = createReplacerFromDirectives(directives); + result = replacer(result); + } + } + } result = result.replace(/export default /g, 'export '); result = result.replace(/export declare /g, 'export '); result = result.replace(/declare /g, ''); + let lines = result.split(/\r\n|\r|\n/); + for (let i = 0; i < lines.length; i++) { + if (/\s*\*/.test(lines[i])) { + // very likely a comment + continue; + } + lines[i] = lines[i].replace(/"/g, '\''); + } + result = lines.join('\n'); if (declaration.kind === ts.SyntaxKind.EnumDeclaration) { result = result.replace(/const enum/, 'enum'); - enums.push(result); + enums.push({ + enumName: declaration.name.getText(sourceFile), + text: result + }); } return result; } @@ -277,6 +309,14 @@ function format(text, endl) { return result; } } +function createReplacerFromDirectives(directives) { + return (str) => { + for (let i = 0; i < directives.length; i++) { + str = str.replace(directives[i][0], directives[i][1]); + } + return str; + }; +} function createReplacer(data) { data = data || ''; let rawDirectives = data.split(';'); @@ -292,12 +332,7 @@ function createReplacer(data) { findStr = '\\b' + findStr + '\\b'; directives.push([new RegExp(findStr, 'g'), replaceStr]); }); - return (str) => { - for (let i = 0; i < directives.length; i++) { - str = str.replace(directives[i][0], directives[i][1]); - } - return str; - }; + return createReplacerFromDirectives(directives); } function generateDeclarationFile(recipe, sourceFileGetter) { const endl = /\r\n/.test(recipe) ? '\r\n' : '\n'; @@ -415,6 +450,15 @@ function generateDeclarationFile(recipe, sourceFileGetter) { resultTxt = resultTxt.split(/\r\n|\n|\r/).join(endl); resultTxt = format(resultTxt, endl); resultTxt = resultTxt.split(/\r\n|\n|\r/).join(endl); + enums.sort((e1, e2) => { + if (e1.enumName < e2.enumName) { + return -1; + } + if (e1.enumName > e2.enumName) { + return 1; + } + return 0; + }); let resultEnums = [ '/*---------------------------------------------------------------------------------------------', ' * Copyright (c) Microsoft Corporation. All rights reserved.', @@ -423,7 +467,7 @@ function generateDeclarationFile(recipe, sourceFileGetter) { '', '// THIS IS A GENERATED FILE. DO NOT EDIT DIRECTLY.', '' - ].concat(enums).join(endl); + ].concat(enums.map(e => e.text)).join(endl); resultEnums = resultEnums.split(/\r\n|\n|\r/).join(endl); resultEnums = format(resultEnums, endl); resultEnums = resultEnums.split(/\r\n|\n|\r/).join(endl); diff --git a/build/monaco/api.ts b/build/monaco/api.ts index 9572655db5..607e6ac853 100644 --- a/build/monaco/api.ts +++ b/build/monaco/api.ts @@ -9,7 +9,7 @@ import * as path from 'path'; import * as fancyLog from 'fancy-log'; import * as ansiColors from 'ansi-colors'; -const dtsv = '2'; +const dtsv = '3'; const tsfmt = require('../../tsfmt.json'); @@ -138,7 +138,7 @@ function isDefaultExport(declaration: ts.InterfaceDeclaration | ts.ClassDeclarat ); } -function getMassagedTopLevelDeclarationText(sourceFile: ts.SourceFile, declaration: TSTopLevelDeclare, importName: string, usage: string[], enums: string[]): string { +function getMassagedTopLevelDeclarationText(sourceFile: ts.SourceFile, declaration: TSTopLevelDeclare, importName: string, usage: string[], enums: IEnumEntry[]): string { let result = getNodeText(sourceFile, declaration); if (declaration.kind === ts.SyntaxKind.InterfaceDeclaration || declaration.kind === ts.SyntaxKind.ClassDeclaration) { let interfaceDeclaration = declaration; @@ -177,14 +177,45 @@ function getMassagedTopLevelDeclarationText(sourceFile: ts.SourceFile, declarati // life.. } }); + } else if (declaration.kind === ts.SyntaxKind.VariableStatement) { + const jsDoc = result.substr(0, declaration.getLeadingTriviaWidth(sourceFile)); + if (jsDoc.indexOf('@monacodtsreplace') >= 0) { + const jsDocLines = jsDoc.split(/\r\n|\r|\n/); + let directives: [RegExp, string][] = []; + for (const jsDocLine of jsDocLines) { + const m = jsDocLine.match(/^\s*\* \/([^/]+)\/([^/]+)\/$/); + if (m) { + directives.push([new RegExp(m[1], 'g'), m[2]]); + } + } + // remove the jsdoc + result = result.substr(jsDoc.length); + if (directives.length > 0) { + // apply replace directives + const replacer = createReplacerFromDirectives(directives); + result = replacer(result); + } + } } result = result.replace(/export default /g, 'export '); result = result.replace(/export declare /g, 'export '); result = result.replace(/declare /g, ''); + let lines = result.split(/\r\n|\r|\n/); + for (let i = 0; i < lines.length; i++) { + if (/\s*\*/.test(lines[i])) { + // very likely a comment + continue; + } + lines[i] = lines[i].replace(/"/g, '\''); + } + result = lines.join('\n'); if (declaration.kind === ts.SyntaxKind.EnumDeclaration) { result = result.replace(/const enum/, 'enum'); - enums.push(result); + enums.push({ + enumName: declaration.name.getText(sourceFile), + text: result + }); } return result; @@ -324,6 +355,15 @@ function format(text: string, endl: string): string { } } +function createReplacerFromDirectives(directives: [RegExp, string][]): (str: string) => string { + return (str: string) => { + for (let i = 0; i < directives.length; i++) { + str = str.replace(directives[i][0], directives[i][1]); + } + return str; + }; +} + function createReplacer(data: string): (str: string) => string { data = data || ''; let rawDirectives = data.split(';'); @@ -341,12 +381,7 @@ function createReplacer(data: string): (str: string) => string { directives.push([new RegExp(findStr, 'g'), replaceStr]); }); - return (str: string) => { - for (let i = 0; i < directives.length; i++) { - str = str.replace(directives[i][0], directives[i][1]); - } - return str; - }; + return createReplacerFromDirectives(directives); } interface ITempResult { @@ -355,6 +390,11 @@ interface ITempResult { enums: string; } +interface IEnumEntry { + enumName: string; + text: string; +} + function generateDeclarationFile(recipe: string, sourceFileGetter: SourceFileGetter): ITempResult | null { const endl = /\r\n/.test(recipe) ? '\r\n' : '\n'; @@ -376,7 +416,7 @@ function generateDeclarationFile(recipe: string, sourceFileGetter: SourceFileGet return importName; }; - let enums: string[] = []; + let enums: IEnumEntry[] = []; let version: string | null = null; lines.forEach(line => { @@ -492,6 +532,16 @@ function generateDeclarationFile(recipe: string, sourceFileGetter: SourceFileGet resultTxt = format(resultTxt, endl); resultTxt = resultTxt.split(/\r\n|\n|\r/).join(endl); + enums.sort((e1, e2) => { + if (e1.enumName < e2.enumName) { + return -1; + } + if (e1.enumName > e2.enumName) { + return 1; + } + return 0; + }); + let resultEnums = [ '/*---------------------------------------------------------------------------------------------', ' * Copyright (c) Microsoft Corporation. All rights reserved.', @@ -500,7 +550,7 @@ function generateDeclarationFile(recipe: string, sourceFileGetter: SourceFileGet '', '// THIS IS A GENERATED FILE. DO NOT EDIT DIRECTLY.', '' - ].concat(enums).join(endl); + ].concat(enums.map(e => e.text)).join(endl); resultEnums = resultEnums.split(/\r\n|\n|\r/).join(endl); resultEnums = format(resultEnums, endl); resultEnums = resultEnums.split(/\r\n|\n|\r/).join(endl); diff --git a/build/monaco/monaco.d.ts.recipe b/build/monaco/monaco.d.ts.recipe index bf1473e951..1f90daf38e 100644 --- a/build/monaco/monaco.d.ts.recipe +++ b/build/monaco/monaco.d.ts.recipe @@ -48,7 +48,7 @@ declare namespace monaco.editor { #include(vs/editor/standalone/common/standaloneThemeService): BuiltinTheme, IStandaloneThemeData, IColors #include(vs/editor/common/modes/supports/tokenization): ITokenThemeRule #include(vs/editor/common/services/webWorker): MonacoWebWorker, IWebWorkerOptions -#include(vs/editor/standalone/browser/standaloneCodeEditor): IActionDescriptor, IStandaloneEditorConstructionOptions, IDiffEditorConstructionOptions, IStandaloneCodeEditor, IStandaloneDiffEditor +#include(vs/editor/standalone/browser/standaloneCodeEditor): IActionDescriptor, IGlobalEditorOptions, IStandaloneEditorConstructionOptions, IDiffEditorConstructionOptions, IStandaloneCodeEditor, IStandaloneDiffEditor export interface ICommandHandler { (...args: any[]): void; } @@ -62,6 +62,7 @@ export interface ICommandHandler { #includeAll(vs/editor/common/editorCommon;editorOptions.=>): IScrollEvent #includeAll(vs/editor/common/model/textModelEvents): #includeAll(vs/editor/common/controller/cursorEvents): +#include(vs/platform/accessibility/common/accessibility): AccessibilitySupport #includeAll(vs/editor/common/config/editorOptions): #includeAll(vs/editor/browser/editorBrowser;editorCommon.=>;editorOptions.=>): #include(vs/editor/common/config/fontInfo): FontInfo, BareFontInfo @@ -87,4 +88,4 @@ declare namespace monaco.worker { } -//dtsv=2 +//dtsv=3 diff --git a/build/monaco/package.json b/build/monaco/package.json index 1ff3e4e72f..70021689eb 100644 --- a/build/monaco/package.json +++ b/build/monaco/package.json @@ -1,7 +1,7 @@ { "name": "monaco-editor-core", "private": true, - "version": "0.18.0", + "version": "0.19.2", "description": "A browser based code editor", "author": "Microsoft Corporation", "license": "MIT", diff --git a/build/package.json b/build/package.json index 07773220ce..d005a4f441 100644 --- a/build/package.json +++ b/build/package.json @@ -7,6 +7,7 @@ "@types/azure": "0.9.19", "@types/debounce": "^1.0.0", "@types/documentdb": "^1.10.5", + "@types/eslint": "4.16.1", "@types/fancy-log": "^1.3.0", "@types/glob": "^7.1.1", "@types/gulp": "^4.0.5", @@ -29,6 +30,8 @@ "@types/through2": "^2.0.34", "@types/underscore": "^1.8.9", "@types/xml2js": "0.0.33", + "@typescript-eslint/experimental-utils": "~2.13.0", + "@typescript-eslint/parser": "^2.12.0", "applicationinsights": "1.0.8", "azure-storage": "^2.1.0", "del": "^3.0.0", @@ -39,6 +42,7 @@ "gulp-uglify": "^3.0.0", "iconv-lite": "0.4.23", "mime": "^1.3.4", + "minimatch": "3.0.4", "minimist": "^1.2.0", "request": "^2.85.0", "rollup": "^1.20.3", @@ -46,8 +50,7 @@ "rollup-plugin-node-resolve": "^5.2.0", "service-downloader": "github:anthonydresser/service-downloader#0.1.7", "terser": "4.3.8", - "tslint": "^5.9.1", - "typescript": "3.7.3", + "typescript": " 3.8.0-beta", "vsce": "1.48.0", "vscode-telemetry-extractor": "^1.5.4", "xml2js": "^0.4.17" diff --git a/build/tslint.json b/build/tslint.json deleted file mode 100644 index 1527527913..0000000000 --- a/build/tslint.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "rules": { - "no-unused-expression": true, - "no-duplicate-variable": true, - "curly": true, - "class-name": true, - "semicolon": [ - true, - "always" - ], - "triple-equals": true - }, - "defaultSeverity": "warning" -} \ No newline at end of file diff --git a/build/win32/i18n/messages.en.isl b/build/win32/i18n/messages.en.isl index 6535824eed..12189c080d 100644 --- a/build/win32/i18n/messages.en.isl +++ b/build/win32/i18n/messages.en.isl @@ -6,4 +6,4 @@ AddToPath=Add to PATH (requires shell restart) RunAfter=Run %1 after installation Other=Other: SourceFile=%1 Source File -OpenWithCodeContextMenu=Open with %1 \ No newline at end of file +OpenWithCodeContextMenu=Open w&ith %1 diff --git a/build/yarn.lock b/build/yarn.lock index 0715af3591..b58fe6a1c8 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -89,6 +89,24 @@ dependencies: "@types/node" "*" +"@types/eslint-visitor-keys@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" + integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== + +"@types/eslint@4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-4.16.1.tgz#19730c9fcb66b6e44742d12b27a603fabfeb2f49" + integrity sha512-lRUXQAULl5geixTiP2K0iYvMUbCkEnuOwvLGjwff12I4ECxoW5QaWML5UUOZ1CvpQLILkddBdMPMZz4ByQizsg== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*": + version "0.0.41" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.41.tgz#fd90754150b57432b72bf560530500597ff04421" + integrity sha512-rIAmXyJlqw4KEBO7+u9gxZZSQHaCNnIzYrnNmYVpgfJhxTqO0brCX0SYpqUTkVI5mwwUwzmtspLBGBKroMeynA== + "@types/estree@0.0.39": version "0.0.39" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" @@ -188,6 +206,11 @@ resolved "https://registry.yarnpkg.com/@types/js-beautify/-/js-beautify-1.8.0.tgz#0369d3d0e1f35a6aec07cb4da2ee2bcda111367c" integrity sha512-/siF86XrwDKLuHe8l7h6NhrAWgLdgqbxmjZv9NvGWmgYRZoTipkjKiWb0SQHy/jcR+ee0GvbG6uGd+LEBMGNvA== +"@types/json-schema@*", "@types/json-schema@^7.0.3": + version "7.0.4" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339" + integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA== + "@types/mime@0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-0.0.29.tgz#fbcfd330573b912ef59eeee14602bface630754b" @@ -328,6 +351,60 @@ resolved "https://registry.yarnpkg.com/@types/xml2js/-/xml2js-0.0.33.tgz#20c5dd6460245284d64a55690015b95e409fb7de" integrity sha1-IMXdZGAkUoTWSlVpABW5XkCft94= +"@typescript-eslint/experimental-utils@2.14.0": + version "2.14.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.14.0.tgz#e9179fa3c44e00b3106b85d7b69342901fb43e3b" + integrity sha512-KcyKS7G6IWnIgl3ZpyxyBCxhkBPV+0a5Jjy2g5HxlrbG2ZLQNFeneIBVXdaBCYOVjvGmGGFKom1kgiAY75SDeQ== + dependencies: + "@types/json-schema" "^7.0.3" + "@typescript-eslint/typescript-estree" "2.14.0" + eslint-scope "^5.0.0" + +"@typescript-eslint/experimental-utils@~2.13.0": + version "2.13.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.13.0.tgz#958614faa6f77599ee2b241740e0ea402482533d" + integrity sha512-+Hss3clwa6aNiC8ZjA45wEm4FutDV5HsVXPl/rDug1THq6gEtOYRGLqS3JlTk7mSnL5TbJz0LpEbzbPnKvY6sw== + dependencies: + "@types/json-schema" "^7.0.3" + "@typescript-eslint/typescript-estree" "2.13.0" + eslint-scope "^5.0.0" + +"@typescript-eslint/parser@^2.12.0": + version "2.14.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.14.0.tgz#30fa0523d86d74172a5e32274558404ba4262cd6" + integrity sha512-haS+8D35fUydIs+zdSf4BxpOartb/DjrZ2IxQ5sR8zyGfd77uT9ZJZYF8+I0WPhzqHmfafUBx8MYpcp8pfaoSA== + dependencies: + "@types/eslint-visitor-keys" "^1.0.0" + "@typescript-eslint/experimental-utils" "2.14.0" + "@typescript-eslint/typescript-estree" "2.14.0" + eslint-visitor-keys "^1.1.0" + +"@typescript-eslint/typescript-estree@2.13.0": + version "2.13.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.13.0.tgz#a2e746867da772c857c13853219fced10d2566bc" + integrity sha512-t21Mg5cc8T3ADEUGwDisHLIubgXKjuNRbkpzDMLb7/JMmgCe/gHM9FaaujokLey+gwTuLF5ndSQ7/EfQqrQx4g== + dependencies: + debug "^4.1.1" + eslint-visitor-keys "^1.1.0" + glob "^7.1.6" + is-glob "^4.0.1" + lodash.unescape "4.0.1" + semver "^6.3.0" + tsutils "^3.17.1" + +"@typescript-eslint/typescript-estree@2.14.0": + version "2.14.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.14.0.tgz#c67698acdc14547f095eeefe908958d93e1a648d" + integrity sha512-pnLpUcMNG7GfFFfNQbEX6f1aPa5fMnH2G9By+A1yovYI4VIOK2DzkaRuUlIkbagpAcrxQHLqovI1YWqEcXyRnA== + dependencies: + debug "^4.1.1" + eslint-visitor-keys "^1.1.0" + glob "^7.1.6" + is-glob "^4.0.1" + lodash.unescape "4.0.1" + semver "^6.3.0" + tsutils "^3.17.1" + abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" @@ -397,13 +474,6 @@ ansi-styles@^2.2.1: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - ansi-wrap@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" @@ -587,15 +657,6 @@ azure-storage@^2.1.0: xml2js "0.2.7" xmlbuilder "0.4.3" -babel-code-frame@^6.22.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" - integrity sha1-Y/1D99weO7fONZR9uP42mj9Yx0s= - dependencies: - chalk "^1.1.3" - esutils "^2.0.2" - js-tokens "^3.0.2" - balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" @@ -747,11 +808,6 @@ buffer@^5.2.1: base64-js "^1.0.2" ieee754 "^1.1.4" -builtin-modules@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" - integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= - builtin-modules@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.1.0.tgz#aad97c15131eb76b65b50ef208e7584cd76a7484" @@ -777,7 +833,7 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= -chalk@^1.0.0, chalk@^1.1.3: +chalk@^1.0.0: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= @@ -788,15 +844,6 @@ chalk@^1.0.0, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.3.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" - integrity sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - cheerio@^1.0.0-rc.1: version "1.0.0-rc.2" resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.2.tgz#4b9f53a81b27e4d5dac31c0ffd0cfa03cc6830db" @@ -876,18 +923,6 @@ collection-visit@^1.0.0: map-visit "^1.0.0" object-visit "^1.0.0" -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - color-support@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" @@ -922,16 +957,16 @@ command-line-args@^5.1.1: lodash.camelcase "^4.3.0" typical "^4.0.0" -commander@^2.12.1, commander@^2.8.1: - version "2.19.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" - integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== - commander@^2.20.0, commander@~2.20.0: version "2.20.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== +commander@^2.8.1: + version "2.19.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" + integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== + commander@~2.8.1: version "2.8.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4" @@ -1204,11 +1239,6 @@ diagnostic-channel@0.2.0: dependencies: semver "^5.3.0" -diff@^3.2.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" - integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== - dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -1317,26 +1347,41 @@ es6-promisify@^5.0.0: dependencies: es6-promise "^4.0.3" -escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: +escape-string-regexp@^1.0.2: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== +eslint-scope@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9" + integrity sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw== + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-visitor-keys@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" + integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== + +esrecurse@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" + integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ== + dependencies: + estraverse "^4.1.0" + +estraverse@^4.1.0, estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== estree-walker@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362" integrity sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w== -esutils@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" - integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs= - eventemitter2@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-5.0.1.tgz#6197a095d5fb6b57e8942f6fd7eaad63a09c9452" @@ -1631,7 +1676,7 @@ glob-parent@^5.0.0: dependencies: is-glob "^4.0.1" -glob@^7.0.3, glob@^7.0.6, glob@^7.1.1: +glob@^7.0.3, glob@^7.0.6: version "7.1.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== @@ -1655,6 +1700,18 @@ glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + globby@^10.0.1: version "10.0.1" resolved "https://registry.yarnpkg.com/globby/-/globby-10.0.1.tgz#4782c34cb75dd683351335c5829cc3420e606b22" @@ -1812,11 +1869,6 @@ has-ansi@^2.0.0: dependencies: ansi-regex "^2.0.0" -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - has-gulplog@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/has-gulplog/-/has-gulplog-0.1.0.tgz#6414c82913697da51590397dafb12f22967811ce" @@ -2240,19 +2292,6 @@ isstream@~0.1.2: resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= -js-tokens@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" - integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= - -js-yaml@^3.7.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" - integrity sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" @@ -2454,6 +2493,11 @@ lodash.templatesettings@^3.0.0: lodash._reinterpolate "^3.0.0" lodash.escape "^3.0.0" +lodash.unescape@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c" + integrity sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw= + lodash@^4.15.0, lodash@^4.17.10: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" @@ -2929,7 +2973,7 @@ path-key@^2.0.0, path-key@^2.0.1: resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= -path-parse@^1.0.5, path-parse@^1.0.6: +path-parse@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== @@ -3203,13 +3247,6 @@ resolve@^1.11.0, resolve@^1.11.1: dependencies: path-parse "^1.0.6" -resolve@^1.3.2: - version "1.8.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26" - integrity sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA== - dependencies: - path-parse "^1.0.5" - ret@~0.1.10: version "0.1.15" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" @@ -3339,6 +3376,11 @@ semver@^5.5.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== +semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + "service-downloader@github:anthonydresser/service-downloader#0.1.7": version "0.1.7" resolved "https://codeload.github.com/anthonydresser/service-downloader/tar.gz/c08de456c9f1af6258061fdc524275b21c6db667" @@ -3598,13 +3640,6 @@ supports-color@^2.0.0: resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - tar-stream@^1.5.2: version "1.6.2" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555" @@ -3746,7 +3781,7 @@ ts-morph@^3.1.3: multimatch "^4.0.0" typescript "^3.0.1" -tslib@^1.8.0, tslib@^1.8.1: +tslib@^1.8.1: version "1.9.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== @@ -3756,28 +3791,10 @@ tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== -tslint@^5.9.1: - version "5.11.0" - resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.11.0.tgz#98f30c02eae3cde7006201e4c33cb08b48581eed" - integrity sha1-mPMMAurjzecAYgHkwzywi0hYHu0= - dependencies: - babel-code-frame "^6.22.0" - builtin-modules "^1.1.1" - chalk "^2.3.0" - commander "^2.12.1" - diff "^3.2.0" - glob "^7.1.1" - js-yaml "^3.7.0" - minimatch "^3.0.4" - resolve "^1.3.2" - semver "^5.3.0" - tslib "^1.8.0" - tsutils "^2.27.2" - -tsutils@^2.27.2: - version "2.29.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99" - integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA== +tsutils@^3.17.1: + version "3.17.1" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" + integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g== dependencies: tslib "^1.8.1" @@ -3806,10 +3823,10 @@ typed-rest-client@^0.9.0: tunnel "0.0.4" underscore "1.8.3" -typescript@3.7.3: - version "3.7.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.3.tgz#b36840668a16458a7025b9eabfad11b66ab85c69" - integrity sha512-Mcr/Qk7hXqFBXMN7p7Lusj1ktCBydylfQM/FZCk5glCNQJrCUKPkMHdo9R0MTFWsC/4kPFvDS0fDPvukfCkFsw== +"typescript@ 3.8.0-beta": + version "3.8.0-beta" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.0-beta.tgz#acdcaf9f24c7e20b1ff0a6329d1e8d63691e2e13" + integrity sha512-mQEmQUJg0CQBhf/GSVnGscKv/jrKsrLxE01AhdjYmBNoXX2Iah3i38ufxXByXacK6Fc5Nr9oMz7MjpjgddiknA== typescript@^3.0.1: version "3.5.3" diff --git a/cgmanifest.json b/cgmanifest.json index c102a04f70..e9dfd3d6ef 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "chromium", "repositoryUrl": "https://chromium.googlesource.com/chromium/src", - "commitHash": "91f08db83c2ce8c722ddf0911ead8f7c473bedfa" + "commitHash": "e4745133a1d3745f066e068b8033c6a269b59caf" } }, "licenseDetail": [ @@ -40,7 +40,7 @@ "SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ], "isOnlyProductionDependency": true, - "version": "76.0.3809.146" + "version": "78.0.3904.130" }, { "component": { @@ -48,11 +48,11 @@ "git": { "name": "nodejs", "repositoryUrl": "https://github.com/nodejs/node", - "commitHash": "64219741218aa87e259cf8257596073b8e747f0a" + "commitHash": "787378879acfb212ed4ff824bf9f767a24a5cb43a" } }, "isOnlyProductionDependency": true, - "version": "12.4.0" + "version": "12.8.1" }, { "component": { @@ -60,12 +60,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "19c705ab80cd6fdccca3d65803ec2c4addb9540a" + "commitHash": "bef0dd868b7d6d32716c319664ed480f2ae17396" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "6.1.6" + "version": "7.1.7" }, { "component": { diff --git a/extensions/admin-tool-ext-win/src/telemetry.ts b/extensions/admin-tool-ext-win/src/telemetry.ts index 02e8ec32b2..57fa4ed217 100644 --- a/extensions/admin-tool-ext-win/src/telemetry.ts +++ b/extensions/admin-tool-ext-win/src/telemetry.ts @@ -3,7 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; import AdsTelemetryReporter from 'ads-extension-telemetry'; import * as Utils from './utils'; @@ -19,8 +18,3 @@ export enum TelemetryViews { SsmsMinGsw = 'SsmsMinGsw', SsmsMinDialog = 'SsmsMinDialog' } - - - - - diff --git a/extensions/agent/src/interfaces.ts b/extensions/agent/src/interfaces.ts index 0e86394477..476110dd1b 100644 --- a/extensions/agent/src/interfaces.ts +++ b/extensions/agent/src/interfaces.ts @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; export enum AgentDialogMode { CREATE = 1, diff --git a/extensions/azurecore/src/azureResource/errors.ts b/extensions/azurecore/src/azureResource/errors.ts index 5f8894a4e4..7400d4c3d5 100644 --- a/extensions/azurecore/src/azureResource/errors.ts +++ b/extensions/azurecore/src/azureResource/errors.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - export class AzureResourceCredentialError extends Error { constructor( message: string, diff --git a/extensions/azurecore/src/azureResource/messageTreeNode.ts b/extensions/azurecore/src/azureResource/messageTreeNode.ts index d598262c53..acd15b9bdc 100644 --- a/extensions/azurecore/src/azureResource/messageTreeNode.ts +++ b/extensions/azurecore/src/azureResource/messageTreeNode.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { NodeInfo } from 'azdata'; diff --git a/extensions/azurecore/src/azureResource/resourceService.ts b/extensions/azurecore/src/azureResource/resourceService.ts index 90dd51111b..b65ed19828 100644 --- a/extensions/azurecore/src/azureResource/resourceService.ts +++ b/extensions/azurecore/src/azureResource/resourceService.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import { extensions, TreeItem } from 'vscode'; import { Account } from 'azdata'; @@ -121,4 +119,4 @@ export class AzureResourceService { this._treeDataProviders[resourceProvider.providerId] = resourceProvider.getTreeDataProvider(); } -} \ No newline at end of file +} diff --git a/extensions/azurecore/src/azureResource/resourceTreeNode.ts b/extensions/azurecore/src/azureResource/resourceTreeNode.ts index 887a33f56a..c951bf1381 100644 --- a/extensions/azurecore/src/azureResource/resourceTreeNode.ts +++ b/extensions/azurecore/src/azureResource/resourceTreeNode.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import { NodeInfo } from 'azdata'; import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import * as nls from 'vscode-nls'; @@ -78,4 +76,4 @@ export class AzureResourceResourceTreeNode extends TreeNode { return this.resourceNodeWithProviderId.resourceNode.treeItem.id; } -} \ No newline at end of file +} diff --git a/extensions/azurecore/src/azureResource/services/accountService.ts b/extensions/azurecore/src/azureResource/services/accountService.ts index f1de0a3edb..4d8acab001 100644 --- a/extensions/azurecore/src/azureResource/services/accountService.ts +++ b/extensions/azurecore/src/azureResource/services/accountService.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import { Event } from 'vscode'; import { Account, DidChangeAccountsParams } from 'azdata'; import { ApiWrapper } from '../../apiWrapper'; diff --git a/extensions/azurecore/src/azureResource/services/cacheService.ts b/extensions/azurecore/src/azureResource/services/cacheService.ts index 826236783c..ef523c5429 100644 --- a/extensions/azurecore/src/azureResource/services/cacheService.ts +++ b/extensions/azurecore/src/azureResource/services/cacheService.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import { ExtensionContext } from 'vscode'; import { IAzureResourceCacheService } from '../interfaces'; @@ -31,4 +29,4 @@ export class AzureResourceCacheService implements IAzureResourceCacheService { private _context: ExtensionContext = undefined; private static readonly cacheKeyPrefix = 'azure.resource.cache'; -} \ No newline at end of file +} diff --git a/extensions/azurecore/src/azureResource/services/subscriptionFilterService.ts b/extensions/azurecore/src/azureResource/services/subscriptionFilterService.ts index b0f1def106..16586401a5 100644 --- a/extensions/azurecore/src/azureResource/services/subscriptionFilterService.ts +++ b/extensions/azurecore/src/azureResource/services/subscriptionFilterService.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import { WorkspaceConfiguration, ConfigurationTarget } from 'vscode'; import { Account } from 'azdata'; diff --git a/extensions/azurecore/src/azureResource/services/tenantService.ts b/extensions/azurecore/src/azureResource/services/tenantService.ts index 42905a6c8e..80158ac9f7 100644 --- a/extensions/azurecore/src/azureResource/services/tenantService.ts +++ b/extensions/azurecore/src/azureResource/services/tenantService.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as request from 'request'; import { azureResource } from '../azure-resource'; @@ -29,4 +27,4 @@ export class AzureResourceTenantService implements IAzureResourceTenantService { return await requestPromisified; } -} \ No newline at end of file +} diff --git a/extensions/azurecore/src/azureResource/tree/accountNotSignedInTreeNode.ts b/extensions/azurecore/src/azureResource/tree/accountNotSignedInTreeNode.ts index 4cc04c7640..902a02be1b 100644 --- a/extensions/azurecore/src/azureResource/tree/accountNotSignedInTreeNode.ts +++ b/extensions/azurecore/src/azureResource/tree/accountNotSignedInTreeNode.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { NodeInfo } from 'azdata'; import * as nls from 'vscode-nls'; diff --git a/extensions/azurecore/src/azureResource/tree/treeChangeHandler.ts b/extensions/azurecore/src/azureResource/tree/treeChangeHandler.ts index f1648cbf22..1f65795aae 100644 --- a/extensions/azurecore/src/azureResource/tree/treeChangeHandler.ts +++ b/extensions/azurecore/src/azureResource/tree/treeChangeHandler.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import { TreeNode } from '../treeNode'; export interface IAzureResourceTreeChangeHandler { diff --git a/extensions/azurecore/src/azureResource/tree/treeProvider.ts b/extensions/azurecore/src/azureResource/tree/treeProvider.ts index 4569fc433e..8326a8d834 100644 --- a/extensions/azurecore/src/azureResource/tree/treeProvider.ts +++ b/extensions/azurecore/src/azureResource/tree/treeProvider.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import { TreeDataProvider, EventEmitter, Event, TreeItem } from 'vscode'; import * as azdata from 'azdata'; import { AppContext } from '../../appContext'; diff --git a/extensions/azurecore/src/azureResource/treeNode.ts b/extensions/azurecore/src/azureResource/treeNode.ts index 560a5dcb5a..18f13d254b 100644 --- a/extensions/azurecore/src/azureResource/treeNode.ts +++ b/extensions/azurecore/src/azureResource/treeNode.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as azdata from 'azdata'; import * as vscode from 'vscode'; diff --git a/extensions/azurecore/src/test/azureResource/messageTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/messageTreeNode.test.ts index 9f8c08e4c3..12d813568f 100644 --- a/extensions/azurecore/src/test/azureResource/messageTreeNode.test.ts +++ b/extensions/azurecore/src/test/azureResource/messageTreeNode.test.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as should from 'should'; import * as vscode from 'vscode'; import 'mocha'; diff --git a/extensions/azurecore/src/test/azureResource/tree/accountNotSignedInTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/accountNotSignedInTreeNode.test.ts index a7539934e7..a9c3e0f4fa 100644 --- a/extensions/azurecore/src/test/azureResource/tree/accountNotSignedInTreeNode.test.ts +++ b/extensions/azurecore/src/test/azureResource/tree/accountNotSignedInTreeNode.test.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as should from 'should'; import * as vscode from 'vscode'; import 'mocha'; diff --git a/extensions/azurecore/src/test/azureResource/tree/subscriptionTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/subscriptionTreeNode.test.ts index d0df58fbdb..d0b627dcd4 100644 --- a/extensions/azurecore/src/test/azureResource/tree/subscriptionTreeNode.test.ts +++ b/extensions/azurecore/src/test/azureResource/tree/subscriptionTreeNode.test.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as should from 'should'; import * as TypeMoq from 'typemoq'; import * as azdata from 'azdata'; diff --git a/extensions/azurecore/src/test/azureResource/tree/treeProvider.test.ts b/extensions/azurecore/src/test/azureResource/tree/treeProvider.test.ts index da974f2d1c..595ba35c63 100644 --- a/extensions/azurecore/src/test/azureResource/tree/treeProvider.test.ts +++ b/extensions/azurecore/src/test/azureResource/tree/treeProvider.test.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as vscode from 'vscode'; import * as should from 'should'; import * as TypeMoq from 'typemoq'; diff --git a/extensions/bat/language-configuration.json b/extensions/bat/language-configuration.json index 2fb5445a34..e463954114 100644 --- a/extensions/bat/language-configuration.json +++ b/extensions/bat/language-configuration.json @@ -11,7 +11,7 @@ ["{", "}"], ["[", "]"], ["(", ")"], - ["\"", "\""] + { "open": "\"", "close": "\"", "notIn": ["string"] } ], "surroundingPairs": [ ["{", "}"], diff --git a/extensions/cms/src/appContext.ts b/extensions/cms/src/appContext.ts index d2061f1fa2..593f9272c2 100644 --- a/extensions/cms/src/appContext.ts +++ b/extensions/cms/src/appContext.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as vscode from 'vscode'; import { ApiWrapper } from './apiWrapper'; import { CmsUtils } from './cmsUtils'; diff --git a/extensions/cms/src/extension.ts b/extensions/cms/src/extension.ts index 09eeba2873..ce08a9daef 100644 --- a/extensions/cms/src/extension.ts +++ b/extensions/cms/src/extension.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as vscode from 'vscode'; import CmsResourceController from './controllers/cmsResourceController'; import { AppContext } from './appContext'; diff --git a/extensions/cms/src/test/cmsResource/tree/cmsResourceEmptyTreeNode.test.ts b/extensions/cms/src/test/cmsResource/tree/cmsResourceEmptyTreeNode.test.ts index 8c08d2e6bc..8a39b917e2 100644 --- a/extensions/cms/src/test/cmsResource/tree/cmsResourceEmptyTreeNode.test.ts +++ b/extensions/cms/src/test/cmsResource/tree/cmsResourceEmptyTreeNode.test.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as should from 'should'; import * as vscode from 'vscode'; import 'mocha'; diff --git a/extensions/cms/src/test/cmsResource/tree/cmsResourceTreeNode.test.ts b/extensions/cms/src/test/cmsResource/tree/cmsResourceTreeNode.test.ts index 6729020af6..6376e5708e 100644 --- a/extensions/cms/src/test/cmsResource/tree/cmsResourceTreeNode.test.ts +++ b/extensions/cms/src/test/cmsResource/tree/cmsResourceTreeNode.test.ts @@ -3,7 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; import * as TypeMoq from 'typemoq'; import * as should from 'should'; import * as vscode from 'vscode'; diff --git a/extensions/cms/src/test/cmsResource/tree/registeredServerTreeNode.test.ts b/extensions/cms/src/test/cmsResource/tree/registeredServerTreeNode.test.ts index c3bc7a84c1..43de96bb59 100644 --- a/extensions/cms/src/test/cmsResource/tree/registeredServerTreeNode.test.ts +++ b/extensions/cms/src/test/cmsResource/tree/registeredServerTreeNode.test.ts @@ -3,7 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; import * as TypeMoq from 'typemoq'; import * as should from 'should'; import * as vscode from 'vscode'; diff --git a/extensions/cms/src/test/cmsResource/tree/serverGroupTreeNode.test.ts b/extensions/cms/src/test/cmsResource/tree/serverGroupTreeNode.test.ts index c5bc31f3eb..7e2bdfd78d 100644 --- a/extensions/cms/src/test/cmsResource/tree/serverGroupTreeNode.test.ts +++ b/extensions/cms/src/test/cmsResource/tree/serverGroupTreeNode.test.ts @@ -3,7 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; import * as TypeMoq from 'typemoq'; import * as should from 'should'; import * as vscode from 'vscode'; diff --git a/extensions/cms/src/test/messageTreeNode.test.ts b/extensions/cms/src/test/messageTreeNode.test.ts index 7447a4385d..46b3798d5e 100644 --- a/extensions/cms/src/test/messageTreeNode.test.ts +++ b/extensions/cms/src/test/messageTreeNode.test.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as should from 'should'; import * as vscode from 'vscode'; import 'mocha'; diff --git a/extensions/configuration-editing/schemas/attachContainer.schema.json b/extensions/configuration-editing/schemas/attachContainer.schema.json index cebd0f2bc3..58e59d72ee 100644 --- a/extensions/configuration-editing/schemas/attachContainer.schema.json +++ b/extensions/configuration-editing/schemas/attachContainer.schema.json @@ -40,7 +40,9 @@ "type": "array", "description": "An array of extensions that should be installed into the container.", "items": { - "type": "string" + "type": "string", + "pattern": "^([a-z0-9A-Z][a-z0-9\\-A-Z]*)\\.([a-z0-9A-Z][a-z0-9\\-A-Z]*)$", + "errorMessage": "Expected format '${publisher}.${name}'. Example: 'vscode.csharp'." } } } diff --git a/extensions/configuration-editing/schemas/devContainer.schema.json b/extensions/configuration-editing/schemas/devContainer.schema.json index aed58455d0..c9c34af63f 100644 --- a/extensions/configuration-editing/schemas/devContainer.schema.json +++ b/extensions/configuration-editing/schemas/devContainer.schema.json @@ -15,7 +15,9 @@ "type": "array", "description": "An array of extensions that should be installed into the container.", "items": { - "type": "string" + "type": "string", + "pattern": "^([a-z0-9A-Z][a-z0-9\\-A-Z]*)\\.([a-z0-9A-Z][a-z0-9\\-A-Z]*)$", + "errorMessage": "Expected format '${publisher}.${name}'. Example: 'vscode.csharp'." } }, "settings": { diff --git a/extensions/configuration-editing/src/settingsDocumentHelper.ts b/extensions/configuration-editing/src/settingsDocumentHelper.ts index b37c984b6a..362799fe39 100644 --- a/extensions/configuration-editing/src/settingsDocumentHelper.ts +++ b/extensions/configuration-editing/src/settingsDocumentHelper.ts @@ -34,7 +34,11 @@ export class SettingsDocument { // files.defaultLanguage if (location.path[0] === 'files.defaultLanguage') { - return this.provideLanguageCompletionItems(location, range); + return this.provideLanguageCompletionItems(location, range).then(items => { + + // Add special item '${activeEditorLanguage}' + return [this.newSimpleCompletionItem(JSON.stringify('${activeEditorLanguage}'), range, localize('activeEditor', "Use the language of the currently active text editor if any")), ...items]; + }); } return this.provideLanguageOverridesCompletionItems(location, position); @@ -153,7 +157,7 @@ export class SettingsDocument { return Promise.resolve(completions); } - private provideLanguageCompletionItems(_location: Location, range: vscode.Range, formatFunc: (string: string) => string = (l) => JSON.stringify(l)): vscode.ProviderResult { + private provideLanguageCompletionItems(_location: Location, range: vscode.Range, formatFunc: (string: string) => string = (l) => JSON.stringify(l)): Thenable { return vscode.languages.getLanguages().then(languages => { const completionItems = []; const configuration = vscode.workspace.getConfiguration(); diff --git a/extensions/dacpac/src/controllers/controllerBase.ts b/extensions/dacpac/src/controllers/controllerBase.ts index eab1571082..69654f4ff5 100644 --- a/extensions/dacpac/src/controllers/controllerBase.ts +++ b/extensions/dacpac/src/controllers/controllerBase.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as vscode from 'vscode'; export default abstract class ControllerBase implements vscode.Disposable { @@ -26,4 +24,3 @@ export default abstract class ControllerBase implements vscode.Disposable { this.deactivate(); } } - diff --git a/extensions/dacpac/src/controllers/mainController.ts b/extensions/dacpac/src/controllers/mainController.ts index 32dcab6751..32e179fc82 100644 --- a/extensions/dacpac/src/controllers/mainController.ts +++ b/extensions/dacpac/src/controllers/mainController.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as azdata from 'azdata'; import ControllerBase from './controllerBase'; import * as vscode from 'vscode'; diff --git a/extensions/dacpac/src/main.ts b/extensions/dacpac/src/main.ts index cac1908f61..fd444f5221 100644 --- a/extensions/dacpac/src/main.ts +++ b/extensions/dacpac/src/main.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; + import * as vscode from 'vscode'; import ControllerBase from './controllers/controllerBase'; diff --git a/extensions/dacpac/src/test/dacpac.test.ts b/extensions/dacpac/src/test/dacpac.test.ts index ef4a43dd08..ab4deb304d 100644 --- a/extensions/dacpac/src/test/dacpac.test.ts +++ b/extensions/dacpac/src/test/dacpac.test.ts @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; import 'mocha'; import * as should from 'should'; @@ -110,4 +109,4 @@ describe('Check for invalid filename tests', function (): void { function formatFileName(filename: string): string { return path.join(os.tmpdir(), filename); -} \ No newline at end of file +} diff --git a/extensions/dacpac/src/wizard/api/models.ts b/extensions/dacpac/src/wizard/api/models.ts index b2daf29ad0..0944b2345e 100644 --- a/extensions/dacpac/src/wizard/api/models.ts +++ b/extensions/dacpac/src/wizard/api/models.ts @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; import * as azdata from 'azdata'; diff --git a/extensions/dacpac/src/wizard/pages/exportConfigPage.ts b/extensions/dacpac/src/wizard/pages/exportConfigPage.ts index 941859fda6..17c912cacb 100644 --- a/extensions/dacpac/src/wizard/pages/exportConfigPage.ts +++ b/extensions/dacpac/src/wizard/pages/exportConfigPage.ts @@ -3,7 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; import * as azdata from 'azdata'; import * as nls from 'vscode-nls'; import * as vscode from 'vscode'; diff --git a/extensions/git/package.json b/extensions/git/package.json index d801ec64f9..3e049ddd7e 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -1367,7 +1367,10 @@ "default": false }, "git.defaultCloneDirectory": { - "type": "string", + "type": [ + "string", + "null" + ], "default": null, "description": "%config.defaultCloneDirectory%" }, diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index e43affda74..6771bda760 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -1580,7 +1580,7 @@ export class CommandCenter { await this._branch(repository, undefined, true); } - private async promptForBranchName(defaultName?: string): Promise { + private async promptForBranchName(defaultName?: string, initialValue?: string): Promise { const config = workspace.getConfiguration('git'); const branchWhitespaceChar = config.get('branchWhitespaceChar')!; const branchValidationRegex = config.get('branchValidationRegex')!; @@ -1591,6 +1591,7 @@ export class CommandCenter { const rawBranchName = defaultName || await window.showInputBox({ placeHolder: localize('branch name', "Branch name"), prompt: localize('provide branch name', "Please provide a branch name"), + value: initialValue, ignoreFocusOut: true, validateInput: (name: string) => { const validateName = new RegExp(branchValidationRegex); @@ -1668,7 +1669,8 @@ export class CommandCenter { @command('git.renameBranch', { repository: true }) async renameBranch(repository: Repository): Promise { - const branchName = await this.promptForBranchName(); + const currentBranchName = repository.HEAD && repository.HEAD.name; + const branchName = await this.promptForBranchName(undefined, currentBranchName); if (!branchName) { return; diff --git a/extensions/git/src/fileSystemProvider.ts b/extensions/git/src/fileSystemProvider.ts index 3ea7558b9a..7bef4f4f5c 100644 --- a/extensions/git/src/fileSystemProvider.ts +++ b/extensions/git/src/fileSystemProvider.ts @@ -8,6 +8,7 @@ import { debounce, throttle } from './decorators'; import { fromGitUri, toGitUri } from './uri'; import { Model, ModelChangeEvent, OriginalResourceChangeEvent } from './model'; import { filterEvent, eventToPromise, isDescendant, pathEquals, EmptyDisposable } from './util'; +import { Repository } from './repository'; interface CacheRow { uri: Uri; @@ -17,6 +18,21 @@ interface CacheRow { const THREE_MINUTES = 1000 * 60 * 3; const FIVE_MINUTES = 1000 * 60 * 5; +function sanitizeRef(ref: string, path: string, repository: Repository): string { + if (ref === '~') { + const fileUri = Uri.file(path); + const uriString = fileUri.toString(); + const [indexStatus] = repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString); + return indexStatus ? '' : 'HEAD'; + } + + if (/^~\d$/.test(ref)) { + return `:${ref[1]}`; + } + + return ref; +} + export class GitFileSystemProvider implements FileSystemProvider { private _onDidChangeFile = new EventEmitter(); @@ -116,15 +132,21 @@ export class GitFileSystemProvider implements FileSystemProvider { return EmptyDisposable; } - stat(uri: Uri): FileStat { - const { submoduleOf } = fromGitUri(uri); + async stat(uri: Uri): Promise { + const { submoduleOf, path, ref } = fromGitUri(uri); const repository = submoduleOf ? this.model.getRepository(submoduleOf) : this.model.getRepository(uri); - if (!repository) { throw FileSystemError.FileNotFound(); } - return { type: FileType.File, size: 0, mtime: this.mtime, ctime: 0 }; + let size = 0; + try { + const details = await repository.getObjectDetails(sanitizeRef(ref, path, repository), path); + size = details.size; + } catch { + // noop + } + return { type: FileType.File, size: size, mtime: this.mtime, ctime: 0 }; } readDirectory(): Thenable<[string, FileType][]> { @@ -136,7 +158,7 @@ export class GitFileSystemProvider implements FileSystemProvider { } async readFile(uri: Uri): Promise { - let { path, ref, submoduleOf } = fromGitUri(uri); + const { path, ref, submoduleOf } = fromGitUri(uri); if (submoduleOf) { const repository = this.model.getRepository(submoduleOf); @@ -165,17 +187,8 @@ export class GitFileSystemProvider implements FileSystemProvider { this.cache.set(uri.toString(), cacheValue); - if (ref === '~') { - const fileUri = Uri.file(path); - const uriString = fileUri.toString(); - const [indexStatus] = repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString); - ref = indexStatus ? '' : 'HEAD'; - } else if (/^~\d$/.test(ref)) { - ref = `:${ref[1]}`; - } - try { - return await repository.buffer(ref, path); + return await repository.buffer(sanitizeRef(ref, path, repository), path); } catch (err) { return new Uint8Array(0); } diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index b0bf44b165..beafc4afcc 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1721,7 +1721,7 @@ export class Repository implements Disposable { if (branchName) { // '{0}' will be replaced by the corresponding key-command later in the process, which is why it needs to stay. - this._sourceControl.inputBox.placeholder = localize('commitMessageWithHeadLabel', "Message ({0} to commit on '{1}')", "{0}", branchName); + this._sourceControl.inputBox.placeholder = localize('commitMessageWithHeadLabel', "Message ({0} to commit on '{1}')", '{0}', branchName); } else { this._sourceControl.inputBox.placeholder = localize('commitMessage', "Message ({0} to commit)"); } diff --git a/extensions/image-preview/media/main.css b/extensions/image-preview/media/main.css index f363cb0a54..28c88563c6 100644 --- a/extensions/image-preview/media/main.css +++ b/extensions/image-preview/media/main.css @@ -94,16 +94,16 @@ body img { } .loading-indicator, -.image-load-error-message { +.image-load-error { display: none; } .loading .loading-indicator, -.error .image-load-error-message { +.error .image-load-error { display: block; } -.image-load-error-message { +.image-load-error { margin: 1em; } diff --git a/extensions/image-preview/media/main.js b/extensions/image-preview/media/main.js index eb42624fd5..f7c905ae82 100644 --- a/extensions/image-preview/media/main.js +++ b/extensions/image-preview/media/main.js @@ -305,6 +305,12 @@ image.src = settings.src; + document.querySelector('.open-file-link').addEventListener('click', () => { + vscode.postMessage({ + type: 'reopen-as-text', + }); + }); + window.addEventListener('message', e => { switch (e.data.type) { case 'setScale': diff --git a/extensions/image-preview/src/preview.ts b/extensions/image-preview/src/preview.ts index d66de373ed..9693e2f04e 100644 --- a/extensions/image-preview/src/preview.ts +++ b/extensions/image-preview/src/preview.ts @@ -73,6 +73,8 @@ class Preview extends Disposable { private _imageBinarySize: number | undefined; private _imageZoom: Scale | undefined; + private readonly emptyPngDataUri = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAEElEQVR42gEFAPr/AP///wAI/AL+Sr4t6gAAAABJRU5ErkJggg=='; + constructor( private readonly extensionRoot: vscode.Uri, private readonly resource: vscode.Uri, @@ -108,6 +110,12 @@ class Preview extends Disposable { this.update(); break; } + + case 'reopen-as-text': + { + vscode.commands.executeCommand('vscode.openWith', resource, 'default', webviewEditor.viewColumn); + break; + } } })); @@ -165,9 +173,9 @@ class Preview extends Disposable { } } - private render() { + private async render() { if (this._previewState !== PreviewState.Disposed) { - this.webviewEditor.webview.html = this.getWebiewContents(); + this.webviewEditor.webview.html = await this.getWebiewContents(); } } @@ -191,11 +199,11 @@ class Preview extends Disposable { } } - private getWebiewContents(): string { + private async getWebiewContents(): Promise { const version = Date.now().toString(); const settings = { isMac: process.platform === 'darwin', - src: this.getResourcePath(this.webviewEditor, this.resource, version), + src: await this.getResourcePath(this.webviewEditor, this.resource, version), }; const nonce = Date.now().toString(); @@ -218,28 +226,28 @@ class Preview extends Disposable {
-
${localize('preview.imageLoadError', "An error occurred while loading the image")}
+
+

${localize('preview.imageLoadError', "An error occurred while loading the image.")}

+ ${localize('preview.imageLoadErrorLink', "Open file using VS Code's standard text/binary editor?")} +
`; } - private getResourcePath(webviewEditor: vscode.WebviewPanel, resource: vscode.Uri, version: string) { - switch (resource.scheme) { - case 'data': - return resource.toString(true); - - case 'git': - // Show blank image - return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAEElEQVR42gEFAPr/AP///wAI/AL+Sr4t6gAAAABJRU5ErkJggg=='; - - default: - // Avoid adding cache busting if there is already a query string - if (resource.query) { - return webviewEditor.webview.asWebviewUri(resource).toString(true); - } - return webviewEditor.webview.asWebviewUri(resource).with({ query: `version=${version}` }).toString(true); + private async getResourcePath(webviewEditor: vscode.WebviewPanel, resource: vscode.Uri, version: string): Promise { + if (resource.scheme === 'gitfs') { + const stat = await vscode.workspace.fs.stat(resource); + if (stat.size === 0) { + return this.emptyPngDataUri; + } } + + // Avoid adding cache busting if there is already a query string + if (resource.query) { + return webviewEditor.webview.asWebviewUri(resource).toString(true); + } + return webviewEditor.webview.asWebviewUri(resource).with({ query: `version=${version}` }).toString(true); } private extensionResource(path: string) { diff --git a/extensions/import/src/constants.ts b/extensions/import/src/constants.ts index 463edd136a..dcaa1848f1 100644 --- a/extensions/import/src/constants.ts +++ b/extensions/import/src/constants.ts @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; export const extensionConfigSectionName = 'flatFileImport'; export const serviceName = 'Flat File Import Service'; @@ -11,4 +10,3 @@ export const configLogDebugInfo = 'logDebugInfo'; export const sqlConfigSectionName = 'sql'; export const serviceCrashLink = 'https://github.com/Microsoft/azuredatastudio/issues/2090'; - diff --git a/extensions/import/src/controllers/controllerBase.ts b/extensions/import/src/controllers/controllerBase.ts index eab1571082..69654f4ff5 100644 --- a/extensions/import/src/controllers/controllerBase.ts +++ b/extensions/import/src/controllers/controllerBase.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as vscode from 'vscode'; export default abstract class ControllerBase implements vscode.Disposable { @@ -26,4 +24,3 @@ export default abstract class ControllerBase implements vscode.Disposable { this.deactivate(); } } - diff --git a/extensions/import/src/controllers/mainController.ts b/extensions/import/src/controllers/mainController.ts index 0afa738898..c04a2ab0c5 100644 --- a/extensions/import/src/controllers/mainController.ts +++ b/extensions/import/src/controllers/mainController.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as constants from '../constants'; import * as azdata from 'azdata'; import ControllerBase from './controllerBase'; diff --git a/extensions/import/src/main.ts b/extensions/import/src/main.ts index cac1908f61..fd444f5221 100644 --- a/extensions/import/src/main.ts +++ b/extensions/import/src/main.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; + import * as vscode from 'vscode'; import ControllerBase from './controllers/controllerBase'; diff --git a/extensions/import/src/wizard/api/importPage.ts b/extensions/import/src/wizard/api/importPage.ts index b4ce56ac90..05aeb318de 100644 --- a/extensions/import/src/wizard/api/importPage.ts +++ b/extensions/import/src/wizard/api/importPage.ts @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; import { ImportDataModel } from './models'; import * as azdata from 'azdata'; diff --git a/extensions/import/src/wizard/api/models.ts b/extensions/import/src/wizard/api/models.ts index c008f8f1ee..e30f30fb7a 100644 --- a/extensions/import/src/wizard/api/models.ts +++ b/extensions/import/src/wizard/api/models.ts @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; import * as azdata from 'azdata'; diff --git a/extensions/import/src/wizard/flatFileWizard.ts b/extensions/import/src/wizard/flatFileWizard.ts index 297c220ea4..b86820a9d8 100644 --- a/extensions/import/src/wizard/flatFileWizard.ts +++ b/extensions/import/src/wizard/flatFileWizard.ts @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import * as azdata from 'azdata'; @@ -133,6 +134,3 @@ export class FlatFileWizard { } - - - diff --git a/extensions/import/src/wizard/pages/fileConfigPage.ts b/extensions/import/src/wizard/pages/fileConfigPage.ts index 76da2c8220..aba902109a 100644 --- a/extensions/import/src/wizard/pages/fileConfigPage.ts +++ b/extensions/import/src/wizard/pages/fileConfigPage.ts @@ -3,7 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; import * as azdata from 'azdata'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; diff --git a/extensions/import/src/wizard/pages/modifyColumnsPage.ts b/extensions/import/src/wizard/pages/modifyColumnsPage.ts index 436a511ba4..78205ffe6b 100644 --- a/extensions/import/src/wizard/pages/modifyColumnsPage.ts +++ b/extensions/import/src/wizard/pages/modifyColumnsPage.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as azdata from 'azdata'; import * as nls from 'vscode-nls'; import { ColumnMetadata, ImportDataModel } from '../api/models'; diff --git a/extensions/integration-tests/src/dacpac.test.ts b/extensions/integration-tests/src/dacpac.test.ts index fb03095dad..aa472fb2a0 100644 --- a/extensions/integration-tests/src/dacpac.test.ts +++ b/extensions/integration-tests/src/dacpac.test.ts @@ -14,6 +14,7 @@ import * as vscode from 'vscode'; import { isTestSetupCompleted } from './testContext'; import { getStandaloneServer } from './testConfig'; import * as assert from 'assert'; +import { promisify } from 'util'; const retryCount = 24; // 2 minutes const dacpac1: string = path.join(__dirname, '../testData/Database1.dacpac'); @@ -56,8 +57,8 @@ if (isTestSetupCompleted()) { // Extract dacpac const folderPath = path.join(os.tmpdir(), 'DacFxTest'); - if (!fs.existsSync(folderPath)) { - fs.mkdirSync(folderPath); + if (!(await promisify(fs.exists)(folderPath))) { + await fs.promises.mkdir(folderPath); } const packageFilePath = path.join(folderPath, `${databaseName}.dacpac`); const extractResult = await dacfxService.extractDacpac(databaseName, packageFilePath, databaseName, '1.0.0.0', ownerUri, azdata.TaskExecutionMode.execute); @@ -102,8 +103,8 @@ if (isTestSetupCompleted()) { // Export bacpac const folderPath = path.join(os.tmpdir(), 'DacFxTest'); - if (!fs.existsSync(folderPath)) { - fs.mkdirSync(folderPath); + if (!(await promisify(fs.exists)(folderPath))) { + await fs.promises.mkdir(folderPath); } const packageFilePath = path.join(folderPath, `${databaseName}.bacpac`); const exportResult = await dacfxService.exportBacpac(databaseName, packageFilePath, ownerUri, azdata.TaskExecutionMode.execute); diff --git a/extensions/integration-tests/src/main.ts b/extensions/integration-tests/src/main.ts index 0bfa7578aa..59cc50ddb8 100644 --- a/extensions/integration-tests/src/main.ts +++ b/extensions/integration-tests/src/main.ts @@ -19,6 +19,7 @@ export function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand('test.setupIntegrationTest', async () => { let extensionInstallersFolder = normalize(join(__dirname, '../extensionInstallers')); console.info(`extensionInstallersFolder=${extensionInstallersFolder}`); + // eslint-disable-next-line no-sync let installers = fs.readdirSync(extensionInstallersFolder); for (let i = 0; i < installers.length; i++) { if (installers[i].endsWith('.vsix')) { diff --git a/extensions/integration-tests/src/notebook.test.ts b/extensions/integration-tests/src/notebook.test.ts index 664a4aa29f..c62aa9b1f3 100644 --- a/extensions/integration-tests/src/notebook.test.ts +++ b/extensions/integration-tests/src/notebook.test.ts @@ -13,7 +13,7 @@ import { getConfigValue, EnvironmentVariable_PYTHON_PATH, TestServerProfile, get import { connectToServer, sleep, testServerProfileToIConnectionProfile } from './utils'; import * as fs from 'fs'; import { stressify } from 'adstest'; -import { isNullOrUndefined } from 'util'; +import { isNullOrUndefined, promisify } from 'util'; if (isTestSetupCompleted()) { suite('Notebook integration test suite', function () { @@ -375,8 +375,8 @@ class NotebookTester { async cleanup(testName: string): Promise { try { let fileName = getFileName(testName + this.invocationCount++); - if (fs.existsSync(fileName)) { - fs.unlinkSync(fileName); + if (await promisify(fs.exists)(fileName)) { + await fs.promises.unlink(fileName); console.log(`"${fileName}" is deleted.`); } await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); diff --git a/extensions/integration-tests/src/notebook.util.ts b/extensions/integration-tests/src/notebook.util.ts index f482ba651a..6f237f4ada 100644 --- a/extensions/integration-tests/src/notebook.util.ts +++ b/extensions/integration-tests/src/notebook.util.ts @@ -164,6 +164,7 @@ export const pythonKernelSpec: azdata.nb.IKernelSpec = { export function writeNotebookToFile(pythonNotebook: azdata.nb.INotebookContents, testName: string): vscode.Uri { let fileName = getFileName(testName); let notebookContentString = JSON.stringify(pythonNotebook); + // eslint-disable-next-line no-sync fs.writeFileSync(fileName, notebookContentString); console.log(`Local file is created: '${fileName}'`); let uri = vscode.Uri.file(fileName); diff --git a/extensions/integration-tests/src/schemaCompare.test.ts b/extensions/integration-tests/src/schemaCompare.test.ts index 2df8d10954..59aef7cfb8 100644 --- a/extensions/integration-tests/src/schemaCompare.test.ts +++ b/extensions/integration-tests/src/schemaCompare.test.ts @@ -15,6 +15,7 @@ import { isTestSetupCompleted } from './testContext'; import * as assert from 'assert'; import { getStandaloneServer } from './testConfig'; import { stressify } from 'adstest'; +import { promisify } from 'util'; let schemaCompareService: mssql.ISchemaCompareService; let dacfxService: mssql.IDacFxService; @@ -91,12 +92,12 @@ class SchemaCompareTester { // save to scmp const filepath = path.join(folderPath, `ads_schemaCompare_${now.getTime().toString()}.scmp`); - if (!fs.existsSync(folderPath)) { - fs.mkdirSync(folderPath); + if (!(await promisify(fs.exists)(folderPath))) { + await fs.promises.mkdir(folderPath); } const saveScmpResult = await schemaCompareService.schemaCompareSaveScmp(source, target, azdata.TaskExecutionMode.execute, null, filepath, [], []); assert(saveScmpResult.success && !saveScmpResult.errorMessage, `Save scmp should succeed. Expected: there should be no error. Actual Error message: "${saveScmpResult.errorMessage}`); - assert(fs.existsSync(filepath), `File ${filepath} is expected to be present`); + assert(await promisify(fs.exists)(filepath), `File ${filepath} is expected to be present`); // open scmp const openScmpResult = await schemaCompareService.schemaCompareOpenScmp(filepath); @@ -165,12 +166,12 @@ class SchemaCompareTester { // save to scmp const filepath = path.join(folderPath, `ads_schemaCompare_${now.getTime().toString()}.scmp`); - if (!fs.existsSync(folderPath)) { - fs.mkdirSync(folderPath); + if (!(await promisify(fs.exists)(folderPath))) { + await fs.promises.mkdir(folderPath); } const saveScmpResult = await schemaCompareService.schemaCompareSaveScmp(source, target, azdata.TaskExecutionMode.execute, null, filepath, [], []); assert(saveScmpResult.success && !saveScmpResult.errorMessage, `Save scmp should succeed. Expected: there should be no error. Actual Error message: "${saveScmpResult.errorMessage}`); - assert(fs.existsSync(filepath), `File ${filepath} is expected to be present`); + assert(promisify(fs.exists)(filepath), `File ${filepath} is expected to be present`); // open scmp const openScmpResult = await schemaCompareService.schemaCompareOpenScmp(filepath); @@ -178,7 +179,7 @@ class SchemaCompareTester { assert(openScmpResult.sourceEndpointInfo.databaseName === source.databaseName, `Expected: source database to be ${source.databaseName}, Actual: ${openScmpResult.sourceEndpointInfo.databaseName}`); assert(openScmpResult.targetEndpointInfo.databaseName === target.databaseName, `Expected: target database to be ${target.databaseName}, Actual: ${openScmpResult.targetEndpointInfo.databaseName}`); - fs.unlinkSync(filepath); + await fs.promises.unlink(filepath); } finally { await utils.deleteDB(server, sourceDB, ownerUri); @@ -237,12 +238,12 @@ class SchemaCompareTester { // save to scmp const filepath = path.join(folderPath, `ads_schemaCompare_${now.getTime().toString()}.scmp`); - if (!fs.existsSync(folderPath)) { - fs.mkdirSync(folderPath); + if (!(await promisify(fs.exists)(folderPath))) { + await fs.promises.mkdir(folderPath); } const saveScmpResult = await schemaCompareService.schemaCompareSaveScmp(source, target, azdata.TaskExecutionMode.execute, null, filepath, [], []); assert(saveScmpResult.success && !saveScmpResult.errorMessage, `Save scmp should succeed. Expected: there should be no error. Actual Error message: "${saveScmpResult.errorMessage}`); - assert(fs.existsSync(filepath), `File ${filepath} is expected to be present`); + assert(await promisify(fs.exists)(filepath), `File ${filepath} is expected to be present`); // open scmp const openScmpResult = await schemaCompareService.schemaCompareOpenScmp(filepath); diff --git a/extensions/integration-tests/src/utils.ts b/extensions/integration-tests/src/utils.ts index a458b83e57..d2e0a910dd 100644 --- a/extensions/integration-tests/src/utils.ts +++ b/extensions/integration-tests/src/utils.ts @@ -8,7 +8,7 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import * as fs from 'fs'; import { TestServerProfile, TestConnectionInfo } from './testConfig'; -import { isNullOrUndefined } from 'util'; +import { isNullOrUndefined, promisify } from 'util'; // default server connection timeout export const DefaultConnectTimeoutInMs: number = 10000; @@ -217,13 +217,13 @@ export async function assertFileGenerationResult(filepath: string, retryCount: n let exists = false; while (retryCount > 0 && !exists) { --retryCount; - exists = fs.existsSync(filepath); + exists = await promisify(fs.exists)(filepath); await sleep(5000); } assert(exists, `File ${filepath} is expected to be present`); - assert(fs.readFileSync(filepath).byteLength > 0, 'File ${filepath} should not be empty'); - fs.unlinkSync(filepath); + assert((await fs.promises.readFile(filepath)).byteLength > 0, 'File ${filepath} should not be empty'); + await fs.promises.unlink(filepath); } /** diff --git a/extensions/json-language-features/client/src/jsonMain.ts b/extensions/json-language-features/client/src/jsonMain.ts index 96f55ae1c5..f28b7d7e2a 100644 --- a/extensions/json-language-features/client/src/jsonMain.ts +++ b/extensions/json-language-features/client/src/jsonMain.ts @@ -149,9 +149,8 @@ export function activate(context: ExtensionContext) { provideCompletionItem(document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature): ProviderResult { function updateRanges(item: CompletionItem) { const range = item.range; - if (range && range.end.isAfter(position) && range.start.isBeforeOrEqual(position)) { - item.range2 = { inserting: new Range(range.start, position), replacing: range }; - item.range = undefined; + if (range instanceof Range && range.end.isAfter(position) && range.start.isBeforeOrEqual(position)) { + item.range = { inserting: new Range(range.start, position), replacing: range }; } } function updateProposals(r: CompletionItem[] | CompletionList | null | undefined): CompletionItem[] | CompletionList | null | undefined { diff --git a/extensions/json-language-features/extension.webpack.config.js b/extensions/json-language-features/extension.webpack.config.js index 7d507e3df0..d9262dca0d 100644 --- a/extensions/json-language-features/extension.webpack.config.js +++ b/extensions/json-language-features/extension.webpack.config.js @@ -9,7 +9,7 @@ const withDefaults = require('../shared.webpack.config'); const path = require('path'); -var webpack = require('webpack'); +const webpack = require('webpack'); const config = withDefaults({ context: path.join(__dirname, 'client'), @@ -25,4 +25,4 @@ const config = withDefaults({ // add plugin, don't replace inherited config.plugins.push(new webpack.IgnorePlugin(/vertx/)); // request-light dependency -module.exports = config; \ No newline at end of file +module.exports = config; diff --git a/extensions/json-language-features/server/extension.webpack.config.js b/extensions/json-language-features/server/extension.webpack.config.js index 5c6783fb6e..bc1319ac35 100644 --- a/extensions/json-language-features/server/extension.webpack.config.js +++ b/extensions/json-language-features/server/extension.webpack.config.js @@ -9,7 +9,7 @@ const withDefaults = require('../../shared.webpack.config'); const path = require('path'); -var webpack = require('webpack'); +const webpack = require('webpack'); const config = withDefaults({ context: path.join(__dirname), diff --git a/extensions/liveshare/src/guestSessionManager.ts b/extensions/liveshare/src/guestSessionManager.ts index a14986a2b4..4fcccf90cd 100644 --- a/extensions/liveshare/src/guestSessionManager.ts +++ b/extensions/liveshare/src/guestSessionManager.ts @@ -11,7 +11,7 @@ import { ConnectionProvider } from './providers/connectionProvider'; import { StatusProvider, LiveShareDocumentState } from './providers/statusProvider'; import { QueryProvider } from './providers/queryProvider'; -declare var require: any; +declare let require: any; let vsls = require('vsls'); export class GuestSessionManager { diff --git a/extensions/liveshare/src/hostSessionManager.ts b/extensions/liveshare/src/hostSessionManager.ts index 63c12a3676..8e2daab918 100644 --- a/extensions/liveshare/src/hostSessionManager.ts +++ b/extensions/liveshare/src/hostSessionManager.ts @@ -10,7 +10,7 @@ import { QueryProvider } from './providers/queryProvider'; import { StatusProvider } from './providers/statusProvider'; import { LiveShareServiceName } from './constants'; -declare var require: any; +declare let require: any; let vsls = require('vsls'); export class HostSessionManager { diff --git a/extensions/liveshare/src/main.ts b/extensions/liveshare/src/main.ts index a433f688f1..159a6a6f6c 100644 --- a/extensions/liveshare/src/main.ts +++ b/extensions/liveshare/src/main.ts @@ -7,7 +7,7 @@ import * as vscode from 'vscode'; import { GuestSessionManager } from './guestSessionManager'; import { HostSessionManager } from './hostSessionManager'; -declare var require: any; +declare let require: any; let vsls = require('vsls'); export async function activate(context: vscode.ExtensionContext) { diff --git a/extensions/machine-learning-services/src/common/constants.ts b/extensions/machine-learning-services/src/common/constants.ts index 81c9d3c415..a64f4b9fa4 100644 --- a/extensions/machine-learning-services/src/common/constants.ts +++ b/extensions/machine-learning-services/src/common/constants.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); diff --git a/extensions/machine-learning-services/src/common/processService.ts b/extensions/machine-learning-services/src/common/processService.ts index 10cbe0513d..f6a95031f4 100644 --- a/extensions/machine-learning-services/src/common/processService.ts +++ b/extensions/machine-learning-services/src/common/processService.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as vscode from 'vscode'; import * as childProcess from 'child_process'; diff --git a/extensions/machine-learning-services/src/common/queryRunner.ts b/extensions/machine-learning-services/src/common/queryRunner.ts index 846a97c22e..10aba9f151 100644 --- a/extensions/machine-learning-services/src/common/queryRunner.ts +++ b/extensions/machine-learning-services/src/common/queryRunner.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as azdata from 'azdata'; import * as nbExtensionApis from '../typings/notebookServices'; import { ApiWrapper } from './apiWrapper'; diff --git a/extensions/machine-learning-services/src/common/utils.ts b/extensions/machine-learning-services/src/common/utils.ts index b36774d23a..9d01ac9e46 100644 --- a/extensions/machine-learning-services/src/common/utils.ts +++ b/extensions/machine-learning-services/src/common/utils.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as UUID from 'vscode-languageclient/lib/utils/uuid'; import * as path from 'path'; import * as os from 'os'; diff --git a/extensions/machine-learning-services/src/controllers/mainController.ts b/extensions/machine-learning-services/src/controllers/mainController.ts index 8ac5a2f1e2..60ca25cb81 100644 --- a/extensions/machine-learning-services/src/controllers/mainController.ts +++ b/extensions/machine-learning-services/src/controllers/mainController.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as vscode from 'vscode'; import * as nbExtensionApis from '../typings/notebookServices'; diff --git a/extensions/machine-learning-services/src/main.ts b/extensions/machine-learning-services/src/main.ts index 264d46081f..1ecf714db1 100644 --- a/extensions/machine-learning-services/src/main.ts +++ b/extensions/machine-learning-services/src/main.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; + import * as vscode from 'vscode'; import MainController from './controllers/mainController'; import { ApiWrapper } from './common/apiWrapper'; diff --git a/extensions/machine-learning-services/src/packageManagement/packageManager.ts b/extensions/machine-learning-services/src/packageManagement/packageManager.ts index 5547640106..bdc48d6268 100644 --- a/extensions/machine-learning-services/src/packageManagement/packageManager.ts +++ b/extensions/machine-learning-services/src/packageManagement/packageManager.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as vscode from 'vscode'; import * as azdata from 'azdata'; import * as nbExtensionApis from '../typings/notebookServices'; diff --git a/extensions/machine-learning-services/src/packageManagement/sqlPythonPackageManageProvider.ts b/extensions/machine-learning-services/src/packageManagement/sqlPythonPackageManageProvider.ts index 133b88ed4a..f79d863049 100644 --- a/extensions/machine-learning-services/src/packageManagement/sqlPythonPackageManageProvider.ts +++ b/extensions/machine-learning-services/src/packageManagement/sqlPythonPackageManageProvider.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as vscode from 'vscode'; import * as azdata from 'azdata'; import * as nbExtensionApis from '../typings/notebookServices'; diff --git a/extensions/machine-learning-services/src/test/common/processService.test.ts b/extensions/machine-learning-services/src/test/common/processService.test.ts index 527c6af277..87731b846f 100644 --- a/extensions/machine-learning-services/src/test/common/processService.test.ts +++ b/extensions/machine-learning-services/src/test/common/processService.test.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as vscode from 'vscode'; import { ProcessService } from '../../common/processService'; import * as utils from '../../common/utils'; diff --git a/extensions/machine-learning-services/src/test/mainController.test.ts b/extensions/machine-learning-services/src/test/mainController.test.ts index ba88538a5e..eebef2d598 100644 --- a/extensions/machine-learning-services/src/test/mainController.test.ts +++ b/extensions/machine-learning-services/src/test/mainController.test.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as vscode from 'vscode'; import * as should from 'should'; import 'mocha'; diff --git a/extensions/machine-learning-services/src/test/packageManagement/packageManager.test.ts b/extensions/machine-learning-services/src/test/packageManagement/packageManager.test.ts index 2f51607b9d..dd3a489628 100644 --- a/extensions/machine-learning-services/src/test/packageManagement/packageManager.test.ts +++ b/extensions/machine-learning-services/src/test/packageManagement/packageManager.test.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as azdata from 'azdata'; import * as should from 'should'; @@ -207,5 +205,3 @@ describe('Package Manager', () => { return packageManager; } }); - - diff --git a/extensions/machine-learning-services/src/test/packageManagement/sqlPythonPackageManageProvider.test.ts b/extensions/machine-learning-services/src/test/packageManagement/sqlPythonPackageManageProvider.test.ts index 7acbc71873..b665e5ed14 100644 --- a/extensions/machine-learning-services/src/test/packageManagement/sqlPythonPackageManageProvider.test.ts +++ b/extensions/machine-learning-services/src/test/packageManagement/sqlPythonPackageManageProvider.test.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as azdata from 'azdata'; import * as should from 'should'; import 'mocha'; diff --git a/extensions/machine-learning-services/src/test/packageManagement/sqlRPackageManageProvider.test.ts b/extensions/machine-learning-services/src/test/packageManagement/sqlRPackageManageProvider.test.ts index 15ed2423e1..1871e93d28 100644 --- a/extensions/machine-learning-services/src/test/packageManagement/sqlRPackageManageProvider.test.ts +++ b/extensions/machine-learning-services/src/test/packageManagement/sqlRPackageManageProvider.test.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as azdata from 'azdata'; import * as should from 'should'; import 'mocha'; diff --git a/extensions/machine-learning-services/src/test/packageManagement/utils.ts b/extensions/machine-learning-services/src/test/packageManagement/utils.ts index 4b363c6eb0..eb80389864 100644 --- a/extensions/machine-learning-services/src/test/packageManagement/utils.ts +++ b/extensions/machine-learning-services/src/test/packageManagement/utils.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as vscode from 'vscode'; import * as azdata from 'azdata'; import * as TypeMoq from 'typemoq'; diff --git a/extensions/machine-learning-services/src/test/queryRunner.test.ts b/extensions/machine-learning-services/src/test/queryRunner.test.ts index ec9bf45623..6183ae593e 100644 --- a/extensions/machine-learning-services/src/test/queryRunner.test.ts +++ b/extensions/machine-learning-services/src/test/queryRunner.test.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as azdata from 'azdata'; import { ApiWrapper } from '../common/apiWrapper'; import * as TypeMoq from 'typemoq'; diff --git a/extensions/machine-learning-services/src/test/serverConfig/serverConfigManager.test.ts b/extensions/machine-learning-services/src/test/serverConfig/serverConfigManager.test.ts index 7d97e92096..82d64f625a 100644 --- a/extensions/machine-learning-services/src/test/serverConfig/serverConfigManager.test.ts +++ b/extensions/machine-learning-services/src/test/serverConfig/serverConfigManager.test.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as azdata from 'azdata'; import { QueryRunner } from '../../common/queryRunner'; import { ApiWrapper } from '../../common/apiWrapper'; diff --git a/extensions/machine-learning-services/src/types.ts b/extensions/machine-learning-services/src/types.ts index 8c622f5a29..d3f568323a 100644 --- a/extensions/machine-learning-services/src/types.ts +++ b/extensions/machine-learning-services/src/types.ts @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; const _typeof = { undefined: 'undefined' diff --git a/extensions/markdown-language-features/preview-src/index.ts b/extensions/markdown-language-features/preview-src/index.ts index ba524893fa..6b13b21893 100644 --- a/extensions/markdown-language-features/preview-src/index.ts +++ b/extensions/markdown-language-features/preview-src/index.ts @@ -10,7 +10,7 @@ import { getEditorLineNumberForPageOffset, scrollToRevealSourceLine, getLineElem import { getSettings, getData } from './settings'; import throttle = require('lodash.throttle'); -declare var acquireVsCodeApi: any; +declare let acquireVsCodeApi: any; let scrollDisabled = true; const marker = new ActiveLineMarker(); diff --git a/extensions/markdown-language-features/src/extension.ts b/extensions/markdown-language-features/src/extension.ts index 3e030be58f..9c3f4b7f81 100644 --- a/extensions/markdown-language-features/src/extension.ts +++ b/extensions/markdown-language-features/src/extension.ts @@ -49,10 +49,7 @@ function registerMarkdownLanguageFeatures( symbolProvider: MDDocumentSymbolProvider, engine: MarkdownEngine ): vscode.Disposable { - const selector: vscode.DocumentSelector = [ - { language: 'markdown', scheme: 'file' }, - { language: 'markdown', scheme: 'untitled' } - ]; + const selector: vscode.DocumentSelector = { language: 'markdown', scheme: '*' }; const charPattern = '(\\p{Alphabetic}|\\p{Number}|\\p{Nonspacing_Mark})'; diff --git a/extensions/mssql/src/appContext.ts b/extensions/mssql/src/appContext.ts index d439f114a3..bcd983338a 100644 --- a/extensions/mssql/src/appContext.ts +++ b/extensions/mssql/src/appContext.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as vscode from 'vscode'; import { ApiWrapper } from './apiWrapper'; diff --git a/extensions/mssql/src/constants.ts b/extensions/mssql/src/constants.ts index f837ff6c66..36493f6abe 100644 --- a/extensions/mssql/src/constants.ts +++ b/extensions/mssql/src/constants.ts @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; export const serviceName = 'SQL Tools Service'; export const providerId = 'MSSQL'; diff --git a/extensions/mssql/src/credentialstore/constants.ts b/extensions/mssql/src/credentialstore/constants.ts index c90dae1353..751dc59085 100644 --- a/extensions/mssql/src/credentialstore/constants.ts +++ b/extensions/mssql/src/credentialstore/constants.ts @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; export const serviceName = 'SerilizationProvider'; export const providerId = 'serilizationProvider'; diff --git a/extensions/mssql/src/credentialstore/contracts.ts b/extensions/mssql/src/credentialstore/contracts.ts index 0dcd28c4f2..1260b204da 100644 --- a/extensions/mssql/src/credentialstore/contracts.ts +++ b/extensions/mssql/src/credentialstore/contracts.ts @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; import { RequestType } from 'vscode-languageclient'; import { Credential } from 'azdata'; diff --git a/extensions/mssql/src/iconProvider.ts b/extensions/mssql/src/iconProvider.ts index 5f06ae94b3..94cba1c30a 100644 --- a/extensions/mssql/src/iconProvider.ts +++ b/extensions/mssql/src/iconProvider.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as azdata from 'azdata'; import * as constants from './constants'; @@ -25,4 +23,4 @@ export class MssqlIconProvider implements azdata.IconProvider { } return Promise.resolve(iconName); } -} \ No newline at end of file +} diff --git a/extensions/mssql/src/objectExplorerNodeProvider/cancelableStream.ts b/extensions/mssql/src/objectExplorerNodeProvider/cancelableStream.ts index 3530be2f8a..b1e318bab1 100644 --- a/extensions/mssql/src/objectExplorerNodeProvider/cancelableStream.ts +++ b/extensions/mssql/src/objectExplorerNodeProvider/cancelableStream.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import { Transform } from 'stream'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; diff --git a/extensions/mssql/src/objectExplorerNodeProvider/providerBase.ts b/extensions/mssql/src/objectExplorerNodeProvider/providerBase.ts index 530cefb103..ee9491d24d 100644 --- a/extensions/mssql/src/objectExplorerNodeProvider/providerBase.ts +++ b/extensions/mssql/src/objectExplorerNodeProvider/providerBase.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as constants from '../constants'; export abstract class ProviderBase { diff --git a/extensions/mssql/src/objectExplorerNodeProvider/treeNodes.ts b/extensions/mssql/src/objectExplorerNodeProvider/treeNodes.ts index 68f7c28c2e..2ffe4ff701 100644 --- a/extensions/mssql/src/objectExplorerNodeProvider/treeNodes.ts +++ b/extensions/mssql/src/objectExplorerNodeProvider/treeNodes.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as azdata from 'azdata'; import * as vscode from 'vscode'; import { ITreeNode } from './types'; diff --git a/extensions/mssql/src/sparkFeature/dialog/sparkJobSubmission/sparkConfigurationTab.ts b/extensions/mssql/src/sparkFeature/dialog/sparkJobSubmission/sparkConfigurationTab.ts index f0b3ab9c5e..240b78e229 100644 --- a/extensions/mssql/src/sparkFeature/dialog/sparkJobSubmission/sparkConfigurationTab.ts +++ b/extensions/mssql/src/sparkFeature/dialog/sparkJobSubmission/sparkConfigurationTab.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as azdata from 'azdata'; import * as nls from 'vscode-nls'; import * as fspath from 'path'; diff --git a/extensions/mssql/src/sqlClusterLookUp.ts b/extensions/mssql/src/sqlClusterLookUp.ts index 9028996efa..aeea6cb977 100644 --- a/extensions/mssql/src/sqlClusterLookUp.ts +++ b/extensions/mssql/src/sqlClusterLookUp.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as azdata from 'azdata'; import * as constants from './constants'; import * as UUID from 'vscode-languageclient/lib/utils/uuid'; diff --git a/extensions/mssql/src/types.ts b/extensions/mssql/src/types.ts index a50f1bfb26..ed5c7afcc6 100644 --- a/extensions/mssql/src/types.ts +++ b/extensions/mssql/src/types.ts @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; const _typeof = { number: 'number', diff --git a/extensions/notebook/src/book/bookTreeItem.ts b/extensions/notebook/src/book/bookTreeItem.ts index eda86b434e..b633b15225 100644 --- a/extensions/notebook/src/book/bookTreeItem.ts +++ b/extensions/notebook/src/book/bookTreeItem.ts @@ -83,7 +83,7 @@ export class BookTreeItem extends vscode.TreeItem { if (this.book.tableOfContents.sections[i].url) { // TODO: Currently only navigating to notebooks. Need to add logic for markdown. let pathToNotebook = path.join(this.book.root, 'content', this.book.tableOfContents.sections[i].url.concat('.ipynb')); - // tslint:disable-next-line:no-sync + // eslint-disable-next-line no-sync if (fs.existsSync(pathToNotebook)) { this._previousUri = pathToNotebook; return; @@ -99,7 +99,7 @@ export class BookTreeItem extends vscode.TreeItem { if (this.book.tableOfContents.sections[i].url) { // TODO: Currently only navigating to notebooks. Need to add logic for markdown. let pathToNotebook = path.join(this.book.root, 'content', this.book.tableOfContents.sections[i].url.concat('.ipynb')); - // tslint:disable-next-line:no-sync + // eslint-disable-next-line no-sync if (fs.existsSync(pathToNotebook)) { this._nextUri = pathToNotebook; return; diff --git a/extensions/notebook/src/common/appContext.ts b/extensions/notebook/src/common/appContext.ts index 4abec27439..df03917215 100644 --- a/extensions/notebook/src/common/appContext.ts +++ b/extensions/notebook/src/common/appContext.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as vscode from 'vscode'; import { ApiWrapper } from './apiWrapper'; diff --git a/extensions/notebook/src/common/constants.ts b/extensions/notebook/src/common/constants.ts index 5d12f4530c..26722b972a 100644 --- a/extensions/notebook/src/common/constants.ts +++ b/extensions/notebook/src/common/constants.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); diff --git a/extensions/notebook/src/common/localizedConstants.ts b/extensions/notebook/src/common/localizedConstants.ts index 9e463f7271..8a8e722af9 100644 --- a/extensions/notebook/src/common/localizedConstants.ts +++ b/extensions/notebook/src/common/localizedConstants.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); @@ -34,6 +32,3 @@ export function openNotebookError(resource: string, error: string): string { ret export function openMarkdownError(resource: string, error: string): string { return localize('openMarkdownError', "Open markdown {0} failed: {1}", resource, error); } export function openUntitledNotebookError(resource: string, error: string): string { return localize('openUntitledNotebookError', "Open untitled notebook {0} as untitled failed: {1}", resource, error); } export function openExternalLinkError(resource: string, error: string): string { return localize('openExternalLinkError', "Open link {0} failed: {1}", resource, error); } - - - diff --git a/extensions/notebook/src/common/notebookUtils.ts b/extensions/notebook/src/common/notebookUtils.ts index 6450738b99..027a6099c7 100644 --- a/extensions/notebook/src/common/notebookUtils.ts +++ b/extensions/notebook/src/common/notebookUtils.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as crypto from 'crypto'; /** diff --git a/extensions/notebook/src/common/promise.ts b/extensions/notebook/src/common/promise.ts index 900b9443ec..68f39d4dcd 100644 --- a/extensions/notebook/src/common/promise.ts +++ b/extensions/notebook/src/common/promise.ts @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; /** * Deferred promise diff --git a/extensions/notebook/src/contracts/content.ts b/extensions/notebook/src/contracts/content.ts index 55792796ec..5580bb5c79 100644 --- a/extensions/notebook/src/contracts/content.ts +++ b/extensions/notebook/src/contracts/content.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - export interface INotebook { readonly cells: ICell[]; diff --git a/extensions/notebook/src/jupyter/common.ts b/extensions/notebook/src/jupyter/common.ts index df07b4b6e1..b61d05e5a1 100644 --- a/extensions/notebook/src/jupyter/common.ts +++ b/extensions/notebook/src/jupyter/common.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as vscode from 'vscode'; export interface IServerInstance { diff --git a/extensions/notebook/src/jupyter/jupyterNotebookManager.ts b/extensions/notebook/src/jupyter/jupyterNotebookManager.ts index 58cdefb0d8..93984d4cfc 100644 --- a/extensions/notebook/src/jupyter/jupyterNotebookManager.ts +++ b/extensions/notebook/src/jupyter/jupyterNotebookManager.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import { nb } from 'azdata'; import * as vscode from 'vscode'; import { ServerConnection, SessionManager } from '@jupyterlab/services'; diff --git a/extensions/notebook/src/jupyter/jupyterServerInstallation.ts b/extensions/notebook/src/jupyter/jupyterServerInstallation.ts index d88e158f9a..cdb85ce50c 100644 --- a/extensions/notebook/src/jupyter/jupyterServerInstallation.ts +++ b/extensions/notebook/src/jupyter/jupyterServerInstallation.ts @@ -222,6 +222,7 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { let pythonSourcePath = path.join(installPath, constants.pythonBundleVersion); if (await utils.exists(pythonSourcePath)) { try { + // eslint-disable-next-line no-sync fs.removeSync(pythonSourcePath); } catch (err) { backgroundOperation.updateStatus(azdata.TaskStatus.InProgress, msgPythonUnpackError); @@ -676,7 +677,7 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { } let condaExePath = this.getCondaExePath(); - // tslint:disable-next-line:no-sync + // eslint-disable-next-line no-sync return fs.existsSync(condaExePath); } @@ -693,7 +694,7 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { let useExistingInstall = JupyterServerInstallation.getExistingPythonSetting(apiWrapper); let pythonExe = JupyterServerInstallation.getPythonExePath(pathSetting, useExistingInstall); - // tslint:disable-next-line:no-sync + // eslint-disable-next-line no-sync return fs.existsSync(pythonExe); } @@ -724,7 +725,7 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { let notebookConfig = apiWrapper.getConfiguration(constants.notebookConfigKey); if (notebookConfig) { let configPythonPath = notebookConfig[constants.pythonPathConfigKey]; - // tslint:disable-next-line:no-sync + // eslint-disable-next-line no-sync if (configPythonPath && fs.existsSync(configPythonPath)) { path = configPythonPath; } diff --git a/extensions/notebook/src/jupyter/jupyterServerManager.ts b/extensions/notebook/src/jupyter/jupyterServerManager.ts index 0464970fa4..fb51954c30 100644 --- a/extensions/notebook/src/jupyter/jupyterServerManager.ts +++ b/extensions/notebook/src/jupyter/jupyterServerManager.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import { nb } from 'azdata'; import * as vscode from 'vscode'; import * as path from 'path'; @@ -168,4 +166,3 @@ export class ServerInstanceFactory { return new PerFolderServerInstance(options); } } - diff --git a/extensions/notebook/src/jupyter/jupyterSettingWriter.ts b/extensions/notebook/src/jupyter/jupyterSettingWriter.ts index 57d852402f..b256590566 100644 --- a/extensions/notebook/src/jupyter/jupyterSettingWriter.ts +++ b/extensions/notebook/src/jupyter/jupyterSettingWriter.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as fs from 'fs-extra'; import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); diff --git a/extensions/notebook/src/jupyter/remoteContentManager.ts b/extensions/notebook/src/jupyter/remoteContentManager.ts index bcde0d7cb9..b271cabe7d 100644 --- a/extensions/notebook/src/jupyter/remoteContentManager.ts +++ b/extensions/notebook/src/jupyter/remoteContentManager.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import { nb } from 'azdata'; import * as vscode from 'vscode'; import { Contents } from '@jupyterlab/services'; diff --git a/extensions/notebook/src/test/book/book.test.ts b/extensions/notebook/src/test/book/book.test.ts index f11936e081..1180969c9f 100644 --- a/extensions/notebook/src/test/book/book.test.ts +++ b/extensions/notebook/src/test/book/book.test.ts @@ -12,7 +12,9 @@ import * as os from 'os'; import * as uuid from 'uuid'; import { BookTreeViewProvider } from '../../book/bookTreeView'; import { BookTreeItem } from '../../book/bookTreeItem'; +import { promisify } from 'util'; import { MockExtensionContext } from '../common/stubs'; +import { exists } from '../../common/utils'; export interface ExpectedBookItem { title: string; @@ -185,8 +187,8 @@ describe('BookTreeViewProviderTests', function() { this.afterAll(async function () { console.log('Removing temporary files...'); - if (fs.existsSync(rootFolderPath)) { - rimraf.sync(rootFolderPath); + if (await exists(rootFolderPath)) { + await promisify(rimraf)(rootFolderPath); } console.log('Successfully removed temporary files.'); }); @@ -228,8 +230,8 @@ describe('BookTreeViewProviderTests', function() { }); this.afterAll(async function () { - if (fs.existsSync(rootFolderPath)) { - rimraf.sync(rootFolderPath); + if (await exists(rootFolderPath)) { + await promisify(rimraf)(rootFolderPath); } }); }); @@ -273,8 +275,8 @@ describe('BookTreeViewProviderTests', function() { }); this.afterAll(async function () { - if (fs.existsSync(rootFolderPath)) { - rimraf.sync(rootFolderPath); + if (await exists(rootFolderPath)) { + await promisify(rimraf)(rootFolderPath); } }); }); @@ -327,12 +329,9 @@ describe('BookTreeViewProviderTests', function() { }); this.afterAll(async function () { - if (fs.existsSync(rootFolderPath)) { - rimraf.sync(rootFolderPath); + if (await exists(rootFolderPath)) { + await promisify(rimraf)(rootFolderPath); } }); }); }); - - - diff --git a/extensions/notebook/src/test/common.ts b/extensions/notebook/src/test/common.ts index ba4696b216..8582847616 100644 --- a/extensions/notebook/src/test/common.ts +++ b/extensions/notebook/src/test/common.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as vscode from 'vscode'; import { IServerInstance } from '../jupyter/common'; diff --git a/extensions/notebook/src/test/common/port.test.ts b/extensions/notebook/src/test/common/port.test.ts index 41cd49a6dc..bdb97ffe3f 100644 --- a/extensions/notebook/src/test/common/port.test.ts +++ b/extensions/notebook/src/test/common/port.test.ts @@ -4,8 +4,6 @@ *--------------------------------------------------------------------------------------------*/ // This code is originally from https://github.com/Microsoft/vscode/blob/master/src/vs/base/test/node/port.test.ts -'use strict'; - import * as assert from 'assert'; import * as net from 'net'; import 'mocha'; @@ -61,4 +59,3 @@ describe('Ports', () => { }, err => done(err)); }); }); - diff --git a/extensions/notebook/src/test/common/querybookUtils.test.ts b/extensions/notebook/src/test/common/querybookUtils.test.ts index 399f272fcf..c29729f69e 100644 --- a/extensions/notebook/src/test/common/querybookUtils.test.ts +++ b/extensions/notebook/src/test/common/querybookUtils.test.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as should from 'should'; import 'mocha'; diff --git a/extensions/notebook/src/test/common/stubs.ts b/extensions/notebook/src/test/common/stubs.ts index da897c6dee..dbc18e68eb 100644 --- a/extensions/notebook/src/test/common/stubs.ts +++ b/extensions/notebook/src/test/common/stubs.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as vscode from 'vscode'; export class MockExtensionContext implements vscode.ExtensionContext { diff --git a/extensions/notebook/src/test/managePackages/localPackageManageProvider.test.ts b/extensions/notebook/src/test/managePackages/localPackageManageProvider.test.ts index ed9c9ba33a..6ee2448ff3 100644 --- a/extensions/notebook/src/test/managePackages/localPackageManageProvider.test.ts +++ b/extensions/notebook/src/test/managePackages/localPackageManageProvider.test.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as vscode from 'vscode'; import * as azdata from 'azdata'; import * as should from 'should'; diff --git a/extensions/notebook/src/test/managePackages/managePackagesDialogModel.test.ts b/extensions/notebook/src/test/managePackages/managePackagesDialogModel.test.ts index 64f4d2d3f4..8fe0deb61e 100644 --- a/extensions/notebook/src/test/managePackages/managePackagesDialogModel.test.ts +++ b/extensions/notebook/src/test/managePackages/managePackagesDialogModel.test.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as should from 'should'; import 'mocha'; import * as TypeMoq from 'typemoq'; diff --git a/extensions/notebook/src/test/model/kernel.test.ts b/extensions/notebook/src/test/model/kernel.test.ts index 53dffc0853..7c0877e170 100644 --- a/extensions/notebook/src/test/model/kernel.test.ts +++ b/extensions/notebook/src/test/model/kernel.test.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as should from 'should'; import * as TypeMoq from 'typemoq'; import { nb } from 'azdata'; @@ -200,4 +198,3 @@ describe('Jupyter Future', function (): void { } }); - diff --git a/extensions/notebook/src/test/model/sessionManager.test.ts b/extensions/notebook/src/test/model/sessionManager.test.ts index 7432b705b4..f581601323 100644 --- a/extensions/notebook/src/test/model/sessionManager.test.ts +++ b/extensions/notebook/src/test/model/sessionManager.test.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as should from 'should'; import * as TypeMoq from 'typemoq'; import { nb } from 'azdata'; diff --git a/extensions/package.json b/extensions/package.json index 65d512cb8a..f2f1225c82 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "3.7.3" + "typescript": "3.7.5" }, "scripts": { "postinstall": "node ./postinstall" diff --git a/extensions/profiler/client/src/data/createSessionData.ts b/extensions/profiler/client/src/data/createSessionData.ts index 85e25f33c8..d2d3328f76 100644 --- a/extensions/profiler/client/src/data/createSessionData.ts +++ b/extensions/profiler/client/src/data/createSessionData.ts @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; import * as azdata from 'azdata'; @@ -23,4 +22,4 @@ export class CreateSessionData { public selectTemplate(name: string): azdata.ProfilerSessionTemplate { return this.templates.find((t) => { return t.name === name; }); } -} \ No newline at end of file +} diff --git a/extensions/profiler/client/src/dialogs/profilerCreateSessionDialog.ts b/extensions/profiler/client/src/dialogs/profilerCreateSessionDialog.ts index 9a5577b214..1695405395 100644 --- a/extensions/profiler/client/src/dialogs/profilerCreateSessionDialog.ts +++ b/extensions/profiler/client/src/dialogs/profilerCreateSessionDialog.ts @@ -3,7 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; import * as nls from 'vscode-nls'; import * as azdata from 'azdata'; import * as vscode from 'vscode'; @@ -122,4 +121,4 @@ export class CreateSessionDialog { vscode.window.showErrorMessage(message); }); } -} \ No newline at end of file +} diff --git a/extensions/profiler/client/src/main.ts b/extensions/profiler/client/src/main.ts index 8160e56b2d..480b0cc335 100644 --- a/extensions/profiler/client/src/main.ts +++ b/extensions/profiler/client/src/main.ts @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; import vscode = require('vscode'); import { MainController } from './mainController'; diff --git a/extensions/profiler/client/src/mainController.ts b/extensions/profiler/client/src/mainController.ts index 93ae68c047..3fa783a544 100644 --- a/extensions/profiler/client/src/mainController.ts +++ b/extensions/profiler/client/src/mainController.ts @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; import * as vscode from 'vscode'; import * as azdata from 'azdata'; diff --git a/extensions/python/package.json b/extensions/python/package.json index c500b586f4..1a7a65b2de 100644 --- a/extensions/python/package.json +++ b/extensions/python/package.json @@ -8,6 +8,7 @@ "engines": { "vscode": "*" }, "activationEvents": ["onLanguage:python"], "main": "./out/pythonMain", + "extensionKind": [ "ui", "workspace" ], "contributes": { "languages": [{ "id": "python", diff --git a/extensions/query-history/yarn.lock b/extensions/query-history/yarn.lock index 9ddd39229d..a52df71861 100644 --- a/extensions/query-history/yarn.lock +++ b/extensions/query-history/yarn.lock @@ -3,9 +3,9 @@ ajv@^6.5.5: - version "6.9.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.9.2.tgz#4927adb83e7f48e5a32b45729744c71ec39c9c7b" - integrity sha512-4UFy0/LgDo7Oa/+wOAlj44tp9K78u38E5/359eSrqEp1Z5PdVfimCcs7SluXMP755RUQu6d2b4AvF0R1C9RZjg== + version "6.10.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" + integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw== dependencies: fast-deep-equal "^2.0.1" fast-json-stable-stringify "^2.0.0" @@ -116,9 +116,9 @@ aws-sign2@~0.7.0: integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= aws4@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" - integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== + version "1.9.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.0.tgz#24390e6ad61386b0a747265754d2a17219de862c" + integrity "sha1-JDkOatYThrCnRyZXVNKhchnehiw= sha512-Uvq6hVe90D0B2WEnUqtdgY1bATGz3mw33nH9Y+dmA+w5DHvUmBgkr5rM/KCHpCsiFNRUfokW/szpPPgMK2hm4A==" balanced-match@^1.0.0: version "1.0.0" @@ -246,9 +246,9 @@ color-support@^1.1.3: integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== combined-stream@^1.0.6, combined-stream@~1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828" - integrity sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w== + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== dependencies: delayed-stream "~1.0.0" @@ -366,9 +366,9 @@ ecc-jsbn@~0.1.1: safer-buffer "^2.1.0" end-of-stream@^1.0.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" - integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q== + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== dependencies: once "^1.4.0" @@ -487,9 +487,9 @@ fast-deep-equal@^2.0.1: integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= fast-json-stable-stringify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" - integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity "sha1-h0v2nG9ATCtdmcSBNBOZ/VWJJjM= sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" fd-slicer@~1.1.0: version "1.1.0" @@ -647,9 +647,9 @@ glob@^5.0.15, glob@^5.0.3: path-is-absolute "^1.0.0" glob@^7.1.3: - version "7.1.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" - integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -879,7 +879,12 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.1, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@^2.0.1, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= @@ -1376,17 +1381,17 @@ micromatch@^2.3.7: parse-glob "^3.0.4" regex-cache "^0.4.2" -mime-db@~1.38.0: - version "1.38.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.38.0.tgz#1a2aab16da9eb167b49c6e4df2d9c68d63d8e2ad" - integrity sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg== +mime-db@1.43.0: + version "1.43.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" + integrity "sha1-ChLgUCZQ5HPXNVNQUOfI9OtPrlg= sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==" mime-types@^2.1.12, mime-types@~2.1.19: - version "2.1.22" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.22.tgz#fe6b355a190926ab7698c9a0556a11199b2199bd" - integrity sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog== + version "2.1.26" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" + integrity "sha1-nJIfwJt+FJpl39wNpNIJlyALCgY= sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==" dependencies: - mime-db "~1.38.0" + mime-db "1.43.0" minimatch@0.3: version "0.3.0" @@ -1816,7 +1821,12 @@ rimraf@2: dependencies: glob "^7.1.3" -safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@^5.0.1, safe-buffer@^5.1.2: + version "5.2.0" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" + integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== @@ -2147,9 +2157,9 @@ util-deprecate@~1.0.1: integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= uuid@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" - integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + version "3.3.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866" + integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ== vali-date@^1.0.0: version "1.0.0" diff --git a/extensions/r/language-configuration.json b/extensions/r/language-configuration.json index 6293d4d61a..dd691e2a6d 100644 --- a/extensions/r/language-configuration.json +++ b/extensions/r/language-configuration.json @@ -11,12 +11,14 @@ ["{", "}"], ["[", "]"], ["(", ")"], - ["\"", "\""] + { "open": "\"", "close": "\"", "notIn": ["string"] }, + { "open": "'", "close": "'", "notIn": ["string"] } ], "surroundingPairs": [ ["{", "}"], ["[", "]"], ["(", ")"], - ["\"", "\""] + ["\"", "\""], + ["'", "'"] ] -} \ No newline at end of file +} diff --git a/extensions/schema-compare/src/controllers/mainController.ts b/extensions/schema-compare/src/controllers/mainController.ts index ca794d4b05..e26f67865c 100644 --- a/extensions/schema-compare/src/controllers/mainController.ts +++ b/extensions/schema-compare/src/controllers/mainController.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as azdata from 'azdata'; import * as vscode from 'vscode'; import { SchemaCompareMainWindow } from '../schemaCompareMainWindow'; diff --git a/extensions/schema-compare/src/dialogs/schemaCompareOptionsDialog.ts b/extensions/schema-compare/src/dialogs/schemaCompareOptionsDialog.ts index 420afdfd8c..3f4ff84636 100644 --- a/extensions/schema-compare/src/dialogs/schemaCompareOptionsDialog.ts +++ b/extensions/schema-compare/src/dialogs/schemaCompareOptionsDialog.ts @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; import * as nls from 'vscode-nls'; import * as azdata from 'azdata'; diff --git a/extensions/schema-compare/src/main.ts b/extensions/schema-compare/src/main.ts index 5e17966ec3..002986d324 100644 --- a/extensions/schema-compare/src/main.ts +++ b/extensions/schema-compare/src/main.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; + import * as vscode from 'vscode'; import MainController from './controllers/mainController'; diff --git a/extensions/schema-compare/src/telemetry.ts b/extensions/schema-compare/src/telemetry.ts index 2f392f703d..90583140c7 100644 --- a/extensions/schema-compare/src/telemetry.ts +++ b/extensions/schema-compare/src/telemetry.ts @@ -3,7 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; import * as vscode from 'vscode'; import TelemetryReporter from 'vscode-extension-telemetry'; diff --git a/extensions/schema-compare/src/test/schemaCompare.test.ts b/extensions/schema-compare/src/test/schemaCompare.test.ts index 2fbdff228d..7fff4ef105 100644 --- a/extensions/schema-compare/src/test/schemaCompare.test.ts +++ b/extensions/schema-compare/src/test/schemaCompare.test.ts @@ -3,8 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - import * as should from 'should'; import * as azdata from 'azdata'; import * as vscode from 'vscode'; diff --git a/extensions/schema-compare/src/utils.ts b/extensions/schema-compare/src/utils.ts index 89a53f82ea..f162354352 100644 --- a/extensions/schema-compare/src/utils.ts +++ b/extensions/schema-compare/src/utils.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; + import * as nls from 'vscode-nls'; import * as azdata from 'azdata'; import * as vscode from 'vscode'; diff --git a/extensions/search-result/syntaxes/generateTMLanguage.js b/extensions/search-result/syntaxes/generateTMLanguage.js index 1024585089..51d9e0ea7c 100644 --- a/extensions/search-result/syntaxes/generateTMLanguage.js +++ b/extensions/search-result/syntaxes/generateTMLanguage.js @@ -1,4 +1,6 @@ // @ts-check +// todo@jackson +/* eslint code-no-unexternalized-strings: 0 */ const mappings = [ ['bat', 'source.batchfile'], diff --git a/extensions/shared.webpack.config.js b/extensions/shared.webpack.config.js index 5db426bbbe..8f55c823ed 100644 --- a/extensions/shared.webpack.config.js +++ b/extensions/shared.webpack.config.js @@ -49,7 +49,7 @@ module.exports = function withDefaults(/**@type WebpackConfig*/extConfig) { loader: 'ts-loader', options: { compilerOptions: { - "sourceMap": true, + 'sourceMap': true, } } }] @@ -64,7 +64,7 @@ module.exports = function withDefaults(/**@type WebpackConfig*/extConfig) { // packaging depends on that and this must always be like it filename: '[name].js', path: path.join(extConfig.context, 'dist'), - libraryTarget: "commonjs", + libraryTarget: 'commonjs', }, // yes, really source maps devtool: 'source-map', diff --git a/extensions/vscode-account/.vscodeignore b/extensions/vscode-account/.vscodeignore new file mode 100644 index 0000000000..ed3f9d37c1 --- /dev/null +++ b/extensions/vscode-account/.vscodeignore @@ -0,0 +1,10 @@ +.vscode/** +.vscode-test/** +out/test/** +src/** +.gitignore +vsc-extension-quickstart.md +**/tsconfig.json +**/tslint.json +**/*.map +**/*.ts \ No newline at end of file diff --git a/extensions/vscode-account/extension.webpack.config.js b/extensions/vscode-account/extension.webpack.config.js new file mode 100644 index 0000000000..aba62f39e2 --- /dev/null +++ b/extensions/vscode-account/extension.webpack.config.js @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check + +'use strict'; + +const withDefaults = require('../shared.webpack.config'); + +module.exports = withDefaults({ + context: __dirname, + entry: { + extension: './src/extension.ts', + }, + externals: { + 'keytar': 'commonjs keytar' + } +}); diff --git a/src/vs/platform/auth/common/auth.css b/extensions/vscode-account/media/auth.css similarity index 100% rename from src/vs/platform/auth/common/auth.css rename to extensions/vscode-account/media/auth.css diff --git a/src/vs/platform/auth/common/auth.html b/extensions/vscode-account/media/auth.html similarity index 99% rename from src/vs/platform/auth/common/auth.html rename to extensions/vscode-account/media/auth.html index 8fe3e50e7b..0fcba4e3c6 100644 --- a/src/vs/platform/auth/common/auth.html +++ b/extensions/vscode-account/media/auth.html @@ -1,6 +1,7 @@ + @@ -8,6 +9,7 @@ + Visual Studio Code @@ -32,4 +34,5 @@ } + diff --git a/extensions/vscode-account/package.json b/extensions/vscode-account/package.json new file mode 100644 index 0000000000..35649821a8 --- /dev/null +++ b/extensions/vscode-account/package.json @@ -0,0 +1,30 @@ +{ + "name": "vscode-account", + "publisher": "vscode", + "displayName": "Account", + "description": "", + "version": "0.0.1", + "engines": { + "vscode": "^1.42.0" + }, + "categories": [ + "Other" + ], + "enableProposedApi": true, + "activationEvents": [ + "*" + ], + "main": "./out/extension.js", + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./" + }, + "devDependencies": { + "typescript": "^3.7.4", + "tslint": "^5.12.1", + "@types/node": "^10.12.21", + "@types/keytar": "^4.0.1", + "@types/vscode": "^1.41.0" + } +} diff --git a/extensions/vscode-account/src/AADHelper.ts b/extensions/vscode-account/src/AADHelper.ts new file mode 100644 index 0000000000..efc29ee51b --- /dev/null +++ b/extensions/vscode-account/src/AADHelper.ts @@ -0,0 +1,265 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as crypto from 'crypto'; +import * as vscode from 'vscode'; +import * as https from 'https'; +import * as querystring from 'querystring'; +import { keychain } from './keychain'; +import { toBase64UrlEncoding } from './utils'; +import { createServer, startServer } from './authServer'; + +const redirectUrl = 'https://vscode-redirect.azurewebsites.net/'; +const loginEndpointUrl = 'https://login.microsoftonline.com/'; +const clientId = 'aebc6443-996d-45c2-90f0-388ff96faa56'; +const scope = 'https://management.core.windows.net/.default offline_access'; +const tenant = 'common'; + +interface IToken { + expiresIn: string; // How long access token is valid, in seconds + accessToken: string; + refreshToken: string; +} + +export const onDidChangeSessions = new vscode.EventEmitter(); + +export class AzureActiveDirectoryService { + private _token: IToken | undefined; + private _refreshTimeout: NodeJS.Timeout | undefined; + + public async initialize(): Promise { + const existingRefreshToken = await keychain.getToken(); + if (existingRefreshToken) { + await this.refreshToken(existingRefreshToken); + } + + this.pollForChange(); + } + + private pollForChange() { + setTimeout(async () => { + const refreshToken = await keychain.getToken(); + // Another window has logged in, generate access token for this instance. + if (refreshToken && !this._token) { + await this.refreshToken(refreshToken); + onDidChangeSessions.fire(); + } + + // Another window has logged out + if (!refreshToken && this._token) { + await this.logout(); + onDidChangeSessions.fire(); + } + + this.pollForChange(); + }, 1000 * 30); + } + + private tokenToAccount(token: IToken): vscode.Session { + return { + id: '', + accessToken: token.accessToken, + displayName: this.getDisplayNameFromToken(token.accessToken) + }; + } + + private getDisplayNameFromToken(accessToken: string): string { + let displayName = 'user@example.com'; + try { + // TODO fixme + displayName = JSON.parse(atob(accessToken.split('.')[1])); + } catch (e) { + // Fall back to example display name + } + + return displayName; + } + + get sessions(): vscode.Session[] { + return this._token ? [this.tokenToAccount(this._token)] : []; + } + + public async login(): Promise { + const nonce = crypto.randomBytes(16).toString('base64'); + const { server, redirectPromise, codePromise } = createServer(nonce); + + let token: IToken | undefined; + try { + const port = await startServer(server); + vscode.env.openExternal(vscode.Uri.parse(`http://localhost:${port}/signin?nonce=${encodeURIComponent(nonce)}`)); + + const redirectReq = await redirectPromise; + if ('err' in redirectReq) { + const { err, res } = redirectReq; + res.writeHead(302, { Location: `/?error=${encodeURIComponent(err && err.message || 'Unknown error')}` }); + res.end(); + throw err; + } + + const host = redirectReq.req.headers.host || ''; + const updatedPortStr = (/^[^:]+:(\d+)$/.exec(Array.isArray(host) ? host[0] : host) || [])[1]; + const updatedPort = updatedPortStr ? parseInt(updatedPortStr, 10) : port; + + const state = `${updatedPort},${encodeURIComponent(nonce)}`; + + const codeVerifier = toBase64UrlEncoding(crypto.randomBytes(32).toString('base64')); + const codeChallenge = toBase64UrlEncoding(crypto.createHash('sha256').update(codeVerifier).digest('base64')); + const loginUrl = `${loginEndpointUrl}${tenant}/oauth2/v2.0/authorize?response_type=code&response_mode=query&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUrl)}&state=${state}&scope=${encodeURIComponent(scope)}&prompt=select_account&code_challenge_method=S256&code_challenge=${codeChallenge}`; + + await redirectReq.res.writeHead(302, { Location: loginUrl }); + redirectReq.res.end(); + + const codeRes = await codePromise; + const res = codeRes.res; + + try { + if ('err' in codeRes) { + throw codeRes.err; + } + token = await this.exchangeCodeForToken(codeRes.code, codeVerifier); + this.setToken(token); + res.writeHead(302, { Location: '/' }); + res.end(); + } catch (err) { + res.writeHead(302, { Location: `/?error=${encodeURIComponent(err && err.message || 'Unknown error')}` }); + res.end(); + } + } finally { + setTimeout(() => { + server.close(); + }, 5000); + } + } + + private async setToken(token: IToken): Promise { + this._token = token; + + if (this._refreshTimeout) { + clearTimeout(this._refreshTimeout); + } + + this._refreshTimeout = setTimeout(async () => { + try { + await this.refreshToken(token.refreshToken); + } catch (e) { + await this.logout(); + } finally { + onDidChangeSessions.fire(); + } + }, 1000 * (parseInt(token.expiresIn) - 10)); + + await keychain.setToken(token.refreshToken); + } + + private async exchangeCodeForToken(code: string, codeVerifier: string): Promise { + return new Promise((resolve: (value: IToken) => void, reject) => { + try { + const postData = querystring.stringify({ + grant_type: 'authorization_code', + code: code, + client_id: clientId, + scope: scope, + code_verifier: codeVerifier, + redirect_uri: redirectUrl + }); + + const tokenUrl = vscode.Uri.parse(`${loginEndpointUrl}${tenant}/oauth2/v2.0/token`); + + const post = https.request({ + host: tokenUrl.authority, + path: tokenUrl.path, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': postData.length + } + }, result => { + const buffer: Buffer[] = []; + result.on('data', (chunk: Buffer) => { + buffer.push(chunk); + }); + result.on('end', () => { + if (result.statusCode === 200) { + const json = JSON.parse(Buffer.concat(buffer).toString()); + resolve({ + expiresIn: json.expires_in, + accessToken: json.access_token, + refreshToken: json.refresh_token + }); + } else { + reject(new Error('Unable to login.')); + } + }); + }); + + post.write(postData); + + post.end(); + post.on('error', err => { + reject(err); + }); + + } catch (e) { + reject(e); + } + }); + } + + private async refreshToken(refreshToken: string): Promise { + return new Promise((resolve: (value: IToken) => void, reject) => { + const postData = querystring.stringify({ + refresh_token: refreshToken, + client_id: clientId, + grant_type: 'refresh_token', + scope: scope + }); + + const post = https.request({ + host: 'login.microsoftonline.com', + path: `/${tenant}/oauth2/v2.0/token`, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': postData.length + } + }, result => { + const buffer: Buffer[] = []; + result.on('data', (chunk: Buffer) => { + buffer.push(chunk); + }); + result.on('end', async () => { + if (result.statusCode === 200) { + const json = JSON.parse(Buffer.concat(buffer).toString()); + const token = { + expiresIn: json.expires_in, + accessToken: json.access_token, + refreshToken: json.refresh_token + }; + this.setToken(token); + resolve(token); + } else { + await this.logout(); + reject(new Error('Refreshing token failed.')); + } + }); + }); + + post.write(postData); + + post.end(); + post.on('error', err => { + reject(err); + }); + }); + } + + public async logout() { + delete this._token; + await keychain.deleteToken(); + if (this._refreshTimeout) { + clearTimeout(this._refreshTimeout); + } + } +} diff --git a/src/vs/platform/auth/electron-browser/authServer.ts b/extensions/vscode-account/src/authServer.ts similarity index 81% rename from src/vs/platform/auth/electron-browser/authServer.ts rename to extensions/vscode-account/src/authServer.ts index aed2ccf204..074b7ddd93 100644 --- a/src/vs/platform/auth/electron-browser/authServer.ts +++ b/extensions/vscode-account/src/authServer.ts @@ -7,14 +7,46 @@ import * as http from 'http'; import * as url from 'url'; import * as fs from 'fs'; import * as net from 'net'; -import { getPathFromAmdModule } from 'vs/base/common/amd'; -import { assertIsDefined } from 'vs/base/common/types'; +import * as path from 'path'; interface Deferred { resolve: (result: T | Promise) => void; reject: (reason: any) => void; } +const _typeof = { + number: 'number', + string: 'string', + undefined: 'undefined', + object: 'object', + function: 'function' +}; + +/** + * @returns whether the provided parameter is undefined. + */ +export function isUndefined(obj: any): obj is undefined { + return typeof (obj) === _typeof.undefined; +} + +/** + * @returns whether the provided parameter is undefined or null. + */ +export function isUndefinedOrNull(obj: any): obj is undefined | null { + return isUndefined(obj) || obj === null; +} + +/** + * Asserts that the argument passed in is neither undefined nor null. + */ +export function assertIsDefined(arg: T | null | undefined): T { + if (isUndefinedOrNull(arg)) { + throw new Error('Assertion Failed: argument is undefined or null'); + } + + return arg; +} + export function createTerminateServer(server: http.Server) { const sockets: Record = {}; let socketCount = 0; @@ -140,10 +172,10 @@ export function createServer(nonce: string) { } break; case '/': - sendFile(res, getPathFromAmdModule(require, '../common/auth.html'), 'text/html; charset=utf-8'); + sendFile(res, path.join(__dirname, '../media/auth.html'), 'text/html; charset=utf-8'); break; case '/auth.css': - sendFile(res, getPathFromAmdModule(require, '../common/auth.css'), 'text/css; charset=utf-8'); + sendFile(res, path.join(__dirname, '../media/auth.css'), 'text/css; charset=utf-8'); break; case '/callback': deferredCode.resolve(callback(nonce, reqUrl) diff --git a/extensions/vscode-account/src/extension.ts b/extensions/vscode-account/src/extension.ts new file mode 100644 index 0000000000..defce54576 --- /dev/null +++ b/extensions/vscode-account/src/extension.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { AzureActiveDirectoryService, onDidChangeSessions } from './AADHelper'; + +export async function activate(context: vscode.ExtensionContext) { + + const loginService = new AzureActiveDirectoryService(); + + await loginService.initialize(); + + vscode.authentication.registerAuthenticationProvider({ + id: 'MSA', + displayName: 'Microsoft Account', // TODO localize + onDidChangeSessions: onDidChangeSessions.event, + getSessions: () => Promise.resolve(loginService.sessions), + login: async () => { + try { + await loginService.login(); + return loginService.sessions[0]!; + } catch (e) { + vscode.window.showErrorMessage(`Logging in failed: ${e}`); + throw e; + } + }, + logout: async (id: string) => { + return loginService.logout(); + } + }); + + return; +} + +// this method is called when your extension is deactivated +export function deactivate() { } diff --git a/extensions/vscode-account/src/keychain.ts b/extensions/vscode-account/src/keychain.ts new file mode 100644 index 0000000000..0e49864035 --- /dev/null +++ b/extensions/vscode-account/src/keychain.ts @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// keytar depends on a native module shipped in vscode, so this is +// how we load it +import * as keytarType from 'keytar'; +import { env } from 'vscode'; + +function getKeytar(): Keytar | undefined { + try { + return require('keytar'); + } catch (err) { + console.log(err); + } + + return undefined; +} + +export type Keytar = { + getPassword: typeof keytarType['getPassword']; + setPassword: typeof keytarType['setPassword']; + deletePassword: typeof keytarType['deletePassword']; +}; + +const SERVICE_ID = `${env.uriScheme}-vscode.login`; +const ACCOUNT_ID = 'account'; + +export class Keychain { + private keytar: Keytar; + + constructor() { + const keytar = getKeytar(); + if (!keytar) { + throw new Error('System keychain unavailable'); + } + + this.keytar = keytar; + } + + async setToken(token: string): Promise { + try { + return await this.keytar.setPassword(SERVICE_ID, ACCOUNT_ID, token); + } catch (e) { + // Ignore + } + } + + async getToken() { + try { + return await this.keytar.getPassword(SERVICE_ID, ACCOUNT_ID); + } catch (e) { + // Ignore + } + } + + async deleteToken() { + try { + return await this.keytar.deletePassword(SERVICE_ID, ACCOUNT_ID); + } catch (e) { + // Ignore + } + } +} + +export const keychain = new Keychain(); diff --git a/extensions/vscode-account/src/utils.ts b/extensions/vscode-account/src/utils.ts new file mode 100644 index 0000000000..912cc56035 --- /dev/null +++ b/extensions/vscode-account/src/utils.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export function toBase64UrlEncoding(base64string: string) { + return base64string.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); // Need to use base64url encoding +} diff --git a/extensions/vscode-account/src/vscode.proposed.d.ts b/extensions/vscode-account/src/vscode.proposed.d.ts new file mode 100644 index 0000000000..891928bde3 --- /dev/null +++ b/extensions/vscode-account/src/vscode.proposed.d.ts @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * This is the place for API experiments and proposals. + * These API are NOT stable and subject to change. They are only available in the Insiders + * distribution and CANNOT be used in published extensions. + * + * To test these API in local environment: + * - Use Insiders release of VS Code. + * - Add `"enableProposedApi": true` to your package.json. + * - Copy this file to your project. + */ + +declare module 'vscode' { + + export interface Session { + id: string; + accessToken: string; + displayName: string; + } + + export interface AuthenticationProvider { + readonly id: string; + readonly displayName: string; + readonly onDidChangeSessions: Event; + + /** + * Returns an array of current sessions. + */ + getSessions(): Promise>; + + /** + * Prompts a user to login. + */ + login(): Promise; + logout(sessionId: string): Promise; + } + + export namespace authentication { + export function registerAuthenticationProvider(provider: AuthenticationProvider): Disposable; + + /** + * Fires with the provider id that was registered or unregistered. + */ + export const onDidRegisterAuthenticationProvider: Event; + export const onDidUnregisterAuthenticationProvider: Event; + + /** + * Fires with the provider id that changed sessions. + */ + export const onDidChangeSessions: Event; + export function login(providerId: string): Promise; + export function logout(providerId: string, accountId: string): Promise; + export function getSessions(providerId: string): Promise | undefined>; + } + + // #region Ben - extension auth flow (desktop+web) + + export namespace env { + + export function asExternalUri(target: Uri): Thenable + } +} diff --git a/extensions/vscode-account/tsconfig.json b/extensions/vscode-account/tsconfig.json new file mode 100644 index 0000000000..46be6dc958 --- /dev/null +++ b/extensions/vscode-account/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "outDir": "out", + "lib": [ + "es6", + "es2016", + "dom" + ], + "typeRoots": [ + "node_modules/@types", + "src/typings" + ], + "sourceMap": true, + "rootDir": "src", + "strict": true, + "noImplicitAny": true + }, + "exclude": [ + "node_modules", + ".vscode-test" + ] +} diff --git a/extensions/vscode-account/yarn.lock b/extensions/vscode-account/yarn.lock new file mode 100644 index 0000000000..4fc295de4b --- /dev/null +++ b/extensions/vscode-account/yarn.lock @@ -0,0 +1,658 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.0.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" + integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g== + dependencies: + "@babel/highlight" "^7.8.3" + +"@babel/highlight@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.8.3.tgz#28f173d04223eaaa59bc1d439a3836e6d1265797" + integrity sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg== + dependencies: + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^4.0.0" + +"@types/keytar@^4.0.1": + version "4.4.2" + resolved "https://registry.yarnpkg.com/@types/keytar/-/keytar-4.4.2.tgz#49ef917d6cbb4f19241c0ab50cd35097b5729b32" + integrity sha512-xtQcDj9ruGnMwvSu1E2BH4SFa5Dv2PvSPd0CKEBLN5hEj/v5YpXJY+B6hAfuKIbvEomD7vJTc/P1s1xPNh2kRw== + dependencies: + keytar "*" + +"@types/node@^10.12.21": + version "10.17.13" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.13.tgz#ccebcdb990bd6139cd16e84c39dc2fb1023ca90c" + integrity sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg== + +"@types/vscode@^1.41.0": + version "1.41.0" + resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.41.0.tgz#b0d75920220f84e07093285e59180c0f11d336cd" + integrity sha512-7SfeY5u9jgiELwxyLB3z7l6l/GbN9CqpCQGkcRlB7tKRFBxzbz2PoBfGrLxI1vRfUCIq5+hg5vtDHExwq5j3+A== + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +aproba@^1.0.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + +are-we-there-yet@~1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" + integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +bl@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-3.0.0.tgz#3611ec00579fd18561754360b21e9f784500ff88" + integrity sha512-EUAyP5UHU5hxF8BPT0LKW8gjYLhq1DQIcneOX/pL/m2Alo+OYDQAJlHq+yseMP50Os2nHXOSic6Ss3vSQeyf4A== + dependencies: + readable-stream "^3.0.1" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +builtin-modules@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= + +chalk@^2.0.0, chalk@^2.3.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chownr@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.3.tgz#42d837d5239688d55f303003a508230fa6727142" + integrity sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw== + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +commander@^2.12.1: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +decompress-response@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986" + integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw== + dependencies: + mimic-response "^2.0.0" + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +end-of-stream@^1.1.0, end-of-stream@^1.4.1: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +expand-template@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" + integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== + +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +github-from-package@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" + integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4= + +glob@^7.1.1: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@~1.3.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.13.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" + integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +keytar@*: + version "5.0.0" + resolved "https://registry.yarnpkg.com/keytar/-/keytar-5.0.0.tgz#c89b6b7a4608fd7af633d9f8474b1a7eb97cbe6f" + integrity sha512-a5UheK59YOlJf9i+2Osaj/kkH6mK0RCHVMtJ84u6ZfbfRIbOJ/H4b5VlOF/LgNHF6s78dRSBzZnvIuPiBKv6wg== + dependencies: + nan "2.14.0" + prebuild-install "5.3.3" + +mimic-response@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.0.0.tgz#996a51c60adf12cb8a87d7fb8ef24c2f3d5ebb46" + integrity sha512-8ilDoEapqA4uQ3TwS0jakGONKXVJqpy+RpM+3b7pLdOjghCrEiGp9SRkFbUHAmZW9vdnrENWHjaweIoTIJExSQ== + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + +minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= + +mkdirp@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +nan@2.14.0: + version "2.14.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" + integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== + +napi-build-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.1.tgz#1381a0f92c39d66bf19852e7873432fc2123e508" + integrity sha512-boQj1WFgQH3v4clhu3mTNfP+vOBxorDlE8EKiMjUlLG3C4qAESnn9AxIOkFgTR2c9LtzNjPrjS60cT27ZKBhaA== + +node-abi@^2.7.0: + version "2.13.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.13.0.tgz#e2f2ec444d0aca3ea1b3874b6de41d1665828f63" + integrity sha512-9HrZGFVTR5SOu3PZAnAY2hLO36aW1wmA+FDsVkr85BTST32TLCA1H/AEcatVRAsWLyXS3bqUDYCAjq5/QGuSTA== + dependencies: + semver "^5.4.1" + +noop-logger@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2" + integrity sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI= + +npmlog@^4.0.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + +object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + +prebuild-install@5.3.3: + version "5.3.3" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-5.3.3.tgz#ef4052baac60d465f5ba6bf003c9c1de79b9da8e" + integrity sha512-GV+nsUXuPW2p8Zy7SarF/2W/oiK8bFQgJcncoJ0d7kRpekEA0ftChjfEaF9/Y+QJEc/wFR7RAEa8lYByuUIe2g== + dependencies: + detect-libc "^1.0.3" + expand-template "^2.0.3" + github-from-package "0.0.0" + minimist "^1.2.0" + mkdirp "^0.5.1" + napi-build-utils "^1.0.1" + node-abi "^2.7.0" + noop-logger "^0.1.1" + npmlog "^4.0.1" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^3.0.3" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + which-pm-runs "^1.0.0" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +rc@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +readable-stream@^2.0.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.0.1, readable-stream@^3.1.1: + version "3.4.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc" + integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +resolve@^1.3.2: + version "1.14.2" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.14.2.tgz#dbf31d0fa98b1f29aa5169783b9c290cb865fea2" + integrity sha512-EjlOBLBO1kxsUxsKjLt7TAECyKW6fOh1VRkykQkKGzcBbjjPIxBqGh0jf7GJ3k/f5mxMqW3htMD3WdTUVtW8HQ== + dependencies: + path-parse "^1.0.6" + +safe-buffer@^5.0.1, safe-buffer@~5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" + integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +semver@^5.3.0, semver@^5.4.1: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + +signal-exit@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= + +simple-concat@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.0.tgz#7344cbb8b6e26fb27d66b2fc86f9f6d5997521c6" + integrity sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY= + +simple-get@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.0.tgz#b45be062435e50d159540b576202ceec40b9c6b3" + integrity sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA== + dependencies: + decompress-response "^4.2.0" + once "^1.3.1" + simple-concat "^1.0.0" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +"string-width@^1.0.2 || 2": + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +tar-fs@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.0.0.tgz#677700fc0c8b337a78bee3623fdc235f21d7afad" + integrity sha512-vaY0obB6Om/fso8a8vakQBzwholQ7v5+uy+tF3Ozvxv1KNezmVQAiWtcNmMHFSFPqL3dJA8ha6gdtFbfX9mcxA== + dependencies: + chownr "^1.1.1" + mkdirp "^0.5.1" + pump "^3.0.0" + tar-stream "^2.0.0" + +tar-stream@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.0.tgz#d1aaa3661f05b38b5acc9b7020efdca5179a2cc3" + integrity sha512-+DAn4Nb4+gz6WZigRzKEZl1QuJVOLtAwwF+WUxy1fJ6X63CaGaUAxJRD2KEn1OMfcbCjySTYpNC6WmfQoIEOdw== + dependencies: + bl "^3.0.0" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + +tslib@^1.8.0, tslib@^1.8.1: + version "1.10.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" + integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== + +tslint@^5.12.1: + version "5.20.1" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.20.1.tgz#e401e8aeda0152bc44dd07e614034f3f80c67b7d" + integrity sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg== + dependencies: + "@babel/code-frame" "^7.0.0" + builtin-modules "^1.1.1" + chalk "^2.3.0" + commander "^2.12.1" + diff "^4.0.1" + glob "^7.1.1" + js-yaml "^3.13.1" + minimatch "^3.0.4" + mkdirp "^0.5.1" + resolve "^1.3.2" + semver "^5.3.0" + tslib "^1.8.0" + tsutils "^2.29.0" + +tsutils@^2.29.0: + version "2.29.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99" + integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA== + dependencies: + tslib "^1.8.1" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +typescript@^3.7.4: + version "3.7.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.4.tgz#1743a5ec5fef6a1fa9f3e4708e33c81c73876c19" + integrity sha512-A25xv5XCtarLwXpcDNZzCGvW2D1S3/bACratYBx2sax8PefsFhlYmkQicKHvpYflFS8if4zne5zT5kpJ7pzuvw== + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +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" + integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs= + +wide-align@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== + dependencies: + string-width "^1.0.2 || 2" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= diff --git a/extensions/vscode-colorize-tests/src/colorizer.test.ts b/extensions/vscode-colorize-tests/src/colorizer.test.ts index aab3a008b1..3abe4fb96c 100644 --- a/extensions/vscode-colorize-tests/src/colorizer.test.ts +++ b/extensions/vscode-colorize-tests/src/colorizer.test.ts @@ -56,7 +56,7 @@ function hasThemeChange(d: any, p: any) : boolean { } } return false; -}; +} suite('colorization', () => { let extensionsFolder = normalize(join(__dirname, '../../')); diff --git a/extensions/vscode-colorize-tests/src/colorizerTestMain.ts b/extensions/vscode-colorize-tests/src/colorizerTestMain.ts index 13a27c60ba..079c77e8e8 100644 --- a/extensions/vscode-colorize-tests/src/colorizerTestMain.ts +++ b/extensions/vscode-colorize-tests/src/colorizerTestMain.ts @@ -15,8 +15,8 @@ export function activate(context: vscode.ExtensionContext): any { const outputChannel = vscode.window.createOutputChannel('Semantic Tokens Test'); - const semanticHighlightProvider: vscode.SemanticTokensProvider = { - provideSemanticTokens(document: vscode.TextDocument): vscode.ProviderResult { + const documentSemanticHighlightProvider: vscode.DocumentSemanticTokensProvider = { + provideDocumentSemanticTokens(document: vscode.TextDocument): vscode.ProviderResult { const builder = new vscode.SemanticTokensBuilder(); function addToken(value: string, startLine: number, startCharacter: number, length: number) { @@ -61,6 +61,6 @@ export function activate(context: vscode.ExtensionContext): any { }; - context.subscriptions.push(vscode.languages.registerSemanticTokensProvider({ pattern: '**/*semantic-test.json' }, semanticHighlightProvider, legend)); + context.subscriptions.push(vscode.languages.registerDocumentSemanticTokensProvider({ pattern: '**/*semantic-test.json' }, documentSemanticHighlightProvider, legend)); } diff --git a/extensions/xml-language-features/extension.webpack.config.js b/extensions/xml-language-features/extension.webpack.config.js index f35561d9f2..1e05faa11d 100644 --- a/extensions/xml-language-features/extension.webpack.config.js +++ b/extensions/xml-language-features/extension.webpack.config.js @@ -14,6 +14,9 @@ module.exports = withDefaults({ resolve: { mainFields: ['module', 'main'] }, + externals: { + 'typescript-vscode-sh-plugin': 'commonjs vscode' // used by build/lib/extensions to know what node_modules to bundle + }, entry: { extension: './src/extension.ts', } diff --git a/extensions/xml/package.json b/extensions/xml/package.json index d76a632b10..77e36951b7 100644 --- a/extensions/xml/package.json +++ b/extensions/xml/package.json @@ -43,6 +43,8 @@ ".publishsettings", ".pubxml", ".pubxml.user", + ".rbxlx", + ".rbxmx", ".rdf", ".rng", ".rss", diff --git a/extensions/xml/xml.language-configuration.json b/extensions/xml/xml.language-configuration.json index 702b6dc6eb..15664cbb4f 100644 --- a/extensions/xml/xml.language-configuration.json +++ b/extensions/xml/xml.language-configuration.json @@ -12,8 +12,8 @@ { "open": "{", "close": "}"}, { "open": "[", "close": "]"}, { "open": "(", "close": ")" }, - { "open": "'", "close": "'" }, - { "open": "\"", "close": "\"" }, + { "open": "\"", "close": "\"", "notIn": ["string"] }, + { "open": "'", "close": "'", "notIn": ["string"] }, { "open": "", "notIn": [ "comment", "string" ]}, { "open": "", "notIn": [ "comment", "string" ]} ], diff --git a/extensions/yarn.lock b/extensions/yarn.lock index a8eb51df65..dc1fa7c2de 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -typescript@3.7.3: - version "3.7.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.3.tgz#b36840668a16458a7025b9eabfad11b66ab85c69" - integrity sha512-Mcr/Qk7hXqFBXMN7p7Lusj1ktCBydylfQM/FZCk5glCNQJrCUKPkMHdo9R0MTFWsC/4kPFvDS0fDPvukfCkFsw== +typescript@3.7.5: + version "3.7.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae" + integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw== diff --git a/package.json b/package.json index 986ecf423e..73f1de48d0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "azuredatastudio", "version": "1.15.0", - "distro": "1e86ab4264f78474c4aac8af258ad78e1d9c9f0d", + "distro": "89db2c733c4515c88a0fcffa070ea0644248f797", "author": { "name": "Microsoft Corporation" }, @@ -24,13 +24,14 @@ "update-localization-extension": "node build/npm/update-localization-extension.js", "smoketest": "cd test/smoke && node test/index.js", "monaco-compile-check": "tsc -p src/tsconfig.monaco.json --noEmit", - "tslint": "node node_modules/tslint/bin/tslint -c tslint-gci.json -p src/tsconfig.json", "strict-null-check": "node node_modules/typescript/bin/tsc -p src/tsconfig.strictNullChecks.json", "strict-null-check-watch": "node node_modules/typescript/bin/tsc -p src/tsconfig.strictNullChecks.json --watch", "strict-initialization-watch": "tsc --watch -p src/tsconfig.json --noEmit --strictPropertyInitialization", + "valid-layers-check": "node build/lib/layersChecker.js", "strict-function-types-watch": "tsc --watch -p src/tsconfig.json --noEmit --strictFunctionTypes", "update-distro": "node build/npm/update-distro.js", - "web": "node scripts/code-web.js" + "web": "node scripts/code-web.js", + "eslint": "eslint -c .eslintrc.json --rulesdir ./build/lib/eslint --ext .ts --ext .js ./src/vs ./src/sql ./extensions" }, "dependencies": { "@angular/animations": "~4.1.3", @@ -56,7 +57,7 @@ "jschardet": "2.1.1", "keytar": "^4.11.0", "native-is-elevated": "0.4.1", - "native-keymap": "2.1.0", + "native-keymap": "2.1.1", "native-watchdog": "1.3.0", "ng2-charts": "^1.6.0", "node-pty": "^0.10.0-beta2", @@ -77,10 +78,10 @@ "vscode-ripgrep": "^1.5.7", "vscode-sqlite3": "4.0.9", "vscode-textmate": "4.4.0", - "xterm": "4.3.0-beta.28.vscode.1", + "xterm": "4.4.0-beta.15", "xterm-addon-search": "0.4.0-beta4", "xterm-addon-web-links": "0.2.1", - "xterm-addon-webgl": "0.4.0-beta.11", + "xterm-addon-webgl": "0.5.0-beta.7", "yauzl": "^2.9.2", "yazl": "^2.4.3", "zone.js": "^0.8.4" @@ -108,6 +109,8 @@ "@types/winreg": "^1.2.30", "@types/yauzl": "^2.9.1", "@types/yazl": "^2.4.2", + "@typescript-eslint/eslint-plugin": "2.3.2", + "@typescript-eslint/parser": "^2.12.0", "ansi-colors": "^3.2.3", "asar": "^0.14.0", "chromium-pickle-js": "^0.2.0", @@ -115,7 +118,9 @@ "coveralls": "^2.11.11", "cson-parser": "^1.3.3", "debounce": "^1.0.0", - "electron": "6.1.6", + "electron": "7.1.7", + "eslint": "6.8.0", + "eslint-plugin-jsdoc": "^19.1.0", "event-stream": "3.3.4", "express": "^4.13.1", "fancy-log": "^1.3.3", @@ -138,17 +143,16 @@ "gulp-replace": "^0.5.4", "gulp-shell": "^0.6.5", "gulp-tsb": "4.0.5", - "gulp-tslint": "^8.1.3", "gulp-untar": "^0.0.7", "gulp-vinyl-zip": "^2.1.2", "husky": "^0.13.1", "innosetup": "5.6.1", "is": "^3.1.0", - "istanbul-lib-coverage": "^2.0.5", - "istanbul-lib-instrument": "^3.3.0", - "istanbul-lib-report": "^2.0.8", - "istanbul-lib-source-maps": "^3.0.6", - "istanbul-reports": "^2.2.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.0", "jsdom-no-contextify": "^3.1.0", "lazy.js": "^0.4.2", "merge-options": "^1.0.1", @@ -168,10 +172,8 @@ "source-map": "^0.4.4", "temp-write": "^3.4.0", "ts-loader": "^4.4.2", - "tslint": "^5.16.0", - "tslint-microsoft-contrib": "^6.0.0", "typemoq": "^0.3.2", - "typescript": "3.7.3", + "typescript": "3.8.0-beta", "typescript-formatter": "7.1.0", "vinyl": "^2.0.0", "vinyl-fs": "^3.0.0", diff --git a/remote/package.json b/remote/package.json index e61f399623..95ea409067 100644 --- a/remote/package.json +++ b/remote/package.json @@ -20,10 +20,10 @@ "vscode-proxy-agent": "^0.5.2", "vscode-ripgrep": "^1.5.7", "vscode-textmate": "4.4.0", - "xterm": "4.3.0-beta.28.vscode.1", + "xterm": "4.4.0-beta.15", "xterm-addon-search": "0.4.0-beta4", "xterm-addon-web-links": "0.2.1", - "xterm-addon-webgl": "0.4.0-beta.11", + "xterm-addon-webgl": "0.5.0-beta.7", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, diff --git a/remote/web/package.json b/remote/web/package.json index 745747611d..5bf2c6c7d9 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -5,9 +5,9 @@ "onigasm-umd": "2.2.5", "semver-umd": "^5.5.5", "vscode-textmate": "4.4.0", - "xterm": "4.3.0-beta.28.vscode.1", + "xterm": "4.4.0-beta.15", "xterm-addon-search": "0.4.0-beta4", "xterm-addon-web-links": "0.2.1", - "xterm-addon-webgl": "0.4.0-beta.11" + "xterm-addon-webgl": "0.5.0-beta.7" } } diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index b11746cd8f..12afd3c3d8 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -41,12 +41,12 @@ xterm-addon-web-links@0.2.1: resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.2.1.tgz#6d1f2ce613e09870badf17615e7a1170a31542b2" integrity sha512-2KnHtiq0IG7hfwv3jw2/jQeH1RBk2d5CH4zvgwQe00rLofSJqSfgnJ7gwowxxpGHrpbPr6Lv4AmH/joaNw2+HQ== -xterm-addon-webgl@0.4.0-beta.11: - version "0.4.0-beta.11" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.4.0-beta.11.tgz#0e4a7242e2353cf74aba55e5a5bdc0b4ec87ad10" - integrity sha512-AteDxm1RFy1WnjY9r5iJSETozLebvUkR+jextdZk/ASsK21vsYK0DuVWwRI8afgiN2hUVhxcxuHEJUOV+CJDQA== +xterm-addon-webgl@0.5.0-beta.7: + version "0.5.0-beta.7" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.5.0-beta.7.tgz#b7b95a362e942ad6f86fa286d7b7bd8ee3e7cf67" + integrity sha512-v6aCvhm1C6mvaurGwUYQfyhb2cAUyuVnzf3Ob/hy5ebtyzUj4wW0N9NbqDEJk67UeMi1lV2xZqrO5gNeTpVqFA== -xterm@4.3.0-beta.28.vscode.1: - version "4.3.0-beta.28.vscode.1" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.3.0-beta.28.vscode.1.tgz#89b85398b5801708e833d08bf92b2124fa128943" - integrity sha512-JNHNZyDtAWnybJTrenPzD6g/yXpHOvPqmjau91Up4onRbjQYMSNlth17SqaES68DKn/+4kcIl2c/RG5SXJjvGw== +xterm@4.4.0-beta.15: + version "4.4.0-beta.15" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.4.0-beta.15.tgz#5897bf79d29d1a2496ccd54665aded28c341b1cc" + integrity sha512-Dvz1CMCYKeoxPF7uIDznbRgUA2Mct49Bq93K2nnrDU0pDMM3Sf1t9fkEyz59wxSx5XEHVdLS80jywsz4sjXBjQ== diff --git a/remote/yarn.lock b/remote/yarn.lock index ef13c7644d..387226ccd2 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -428,15 +428,15 @@ xterm-addon-web-links@0.2.1: resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.2.1.tgz#6d1f2ce613e09870badf17615e7a1170a31542b2" integrity sha512-2KnHtiq0IG7hfwv3jw2/jQeH1RBk2d5CH4zvgwQe00rLofSJqSfgnJ7gwowxxpGHrpbPr6Lv4AmH/joaNw2+HQ== -xterm-addon-webgl@0.4.0-beta.11: - version "0.4.0-beta.11" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.4.0-beta.11.tgz#0e4a7242e2353cf74aba55e5a5bdc0b4ec87ad10" - integrity sha512-AteDxm1RFy1WnjY9r5iJSETozLebvUkR+jextdZk/ASsK21vsYK0DuVWwRI8afgiN2hUVhxcxuHEJUOV+CJDQA== +xterm-addon-webgl@0.5.0-beta.7: + version "0.5.0-beta.7" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.5.0-beta.7.tgz#b7b95a362e942ad6f86fa286d7b7bd8ee3e7cf67" + integrity sha512-v6aCvhm1C6mvaurGwUYQfyhb2cAUyuVnzf3Ob/hy5ebtyzUj4wW0N9NbqDEJk67UeMi1lV2xZqrO5gNeTpVqFA== -xterm@4.3.0-beta.28.vscode.1: - version "4.3.0-beta.28.vscode.1" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.3.0-beta.28.vscode.1.tgz#89b85398b5801708e833d08bf92b2124fa128943" - integrity sha512-JNHNZyDtAWnybJTrenPzD6g/yXpHOvPqmjau91Up4onRbjQYMSNlth17SqaES68DKn/+4kcIl2c/RG5SXJjvGw== +xterm@4.4.0-beta.15: + version "4.4.0-beta.15" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.4.0-beta.15.tgz#5897bf79d29d1a2496ccd54665aded28c341b1cc" + integrity sha512-Dvz1CMCYKeoxPF7uIDznbRgUA2Mct49Bq93K2nnrDU0pDMM3Sf1t9fkEyz59wxSx5XEHVdLS80jywsz4sjXBjQ== yauzl@^2.9.2: version "2.10.0" diff --git a/scripts/test-integration.bat b/scripts/test-integration.bat index 57cc0e0f9c..61ba87ef63 100755 --- a/scripts/test-integration.bat +++ b/scripts/test-integration.bat @@ -23,6 +23,11 @@ if "%INTEGRATION_TEST_ELECTRON_PATH%"=="" ( call yarn gulp compile-extension:html-language-features-server call yarn gulp compile-extension:json-language-features-server + :: Configuration for more verbose output + set VSCODE_CLI=1 + set ELECTRON_ENABLE_LOGGING=1 + set ELECTRON_ENABLE_STACK_DUMPING=1 + echo "Running integration tests with '%INTEGRATION_TEST_ELECTRON_PATH%' as build." ) diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index 424e656b68..32498a378f 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -30,6 +30,11 @@ else yarn gulp compile-extension:html-language-features-server yarn gulp compile-extension:json-language-features-server + # Configuration for more verbose output + export VSCODE_CLI=1 + export ELECTRON_ENABLE_STACK_DUMPING=1 + export ELECTRON_ENABLE_LOGGING=1 + echo "Running integration tests with '$INTEGRATION_TEST_ELECTRON_PATH' as build." fi @@ -43,7 +48,6 @@ fi "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/markdown-language-features/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/markdown-language-features --extensionTestsPath=$ROOT/extensions/markdown-language-features/out/test --disable-telemetry --disable-crash-reporter --disable-updates --disable-extensions --skip-getting-started --user-data-dir=$VSCODEUSERDATADIR "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/azurecore/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/azurecore --extensionTestsPath=$ROOT/extensions/azurecore/out/test --disable-telemetry --disable-crash-reporter --disable-updates --disable-extensions --skip-getting-started --user-data-dir=$VSCODEUSERDATADIR - mkdir -p $ROOT/extensions/emmet/test-fixtures "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/emmet/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/emmet --extensionTestsPath=$ROOT/extensions/emmet/out/test --disable-telemetry --disable-crash-reporter --disable-updates --disable-extensions --skip-getting-started --user-data-dir=$VSCODEUSERDATADIR rm -rf $ROOT/extensions/emmet/test-fixtures diff --git a/src/.eslintrc b/src/.eslintrc deleted file mode 100644 index c25b0d558c..0000000000 --- a/src/.eslintrc +++ /dev/null @@ -1,19 +0,0 @@ -{ - "parserOptions": { - "ecmaVersion": 6 - }, - "env": { - "node": true, - "es6": true, - "browser": true, - "amd": true - }, - "rules": { - "no-console": 0, - "no-cond-assign": 0, - "no-unused-vars": "error", - "no-extra-semi": "error", - "semi": "error", - "no-inner-declarations": 0 - } -} \ No newline at end of file diff --git a/src/sql/platform/clipboard/browser/clipboardService.ts b/src/sql/platform/clipboard/browser/clipboardService.ts index 137dafd57d..82a8ba42b0 100644 --- a/src/sql/platform/clipboard/browser/clipboardService.ts +++ b/src/sql/platform/clipboard/browser/clipboardService.ts @@ -71,6 +71,7 @@ export class BrowserClipboardService implements IClipboardService { } readTextSync(): string | undefined { + // eslint-disable-next-line no-sync return this._vsClipboardService.readTextSync(); } } diff --git a/src/sql/setup.js b/src/sql/setup.js index 043900a0af..3dd83b6d4f 100644 --- a/src/sql/setup.js +++ b/src/sql/setup.js @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -define(["require", "exports"], function (require) { +define(['require', 'exports'], function (require) { const jquerylib = require.__$__nodeRequire('jquery'); window['jQuery'] = jquerylib; @@ -21,8 +21,8 @@ define(["require", "exports"], function (require) { require.__$__nodeRequire('zone.js'); require.__$__nodeRequire('zone.js/dist/zone-error'); require.__$__nodeRequire('chart.js'); - window["Zone"]["__zone_symbol__ignoreConsoleErrorUncaughtError"] = true; - window["Zone"]["__zone_symbol__unhandledPromiseRejectionHandler"] = e => setImmediate(() => { + window['Zone']['__zone_symbol__ignoreConsoleErrorUncaughtError'] = true; + window['Zone']['__zone_symbol__unhandledPromiseRejectionHandler'] = e => setImmediate(() => { window.dispatchEvent(new PromiseRejectionEvent('unhandledrejection', e)); }); // let window handle this }); diff --git a/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts b/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts index c3925d3e4d..58c0483dba 100644 --- a/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts +++ b/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts @@ -458,7 +458,7 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements }; let isUntitled: boolean = uri.scheme === Schemas.untitled; - const fileInput = isUntitled ? this._untitledEditorService.createOrGet(uri, 'notebook', options.initialContent) : + const fileInput = isUntitled ? this._untitledEditorService.create({ associatedResource: uri, mode: 'notebook', initialValue: options.initialContent }) : this._editorService.createInput({ resource: uri, mode: 'notebook' }); let input: NotebookInput; if (isUntitled) { diff --git a/src/sql/workbench/browser/modal/optionsDialog.ts b/src/sql/workbench/browser/modal/optionsDialog.ts index dd0c0744ef..9c54e65b06 100644 --- a/src/sql/workbench/browser/modal/optionsDialog.ts +++ b/src/sql/workbench/browser/modal/optionsDialog.ts @@ -44,9 +44,10 @@ export class CategoryView extends ViewPane { @IKeybindingService keybindingService: IKeybindingService, @IContextMenuService contextMenuService: IContextMenuService, @IConfigurationService configurationService: IConfigurationService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @IInstantiationService instantiationService: IInstantiationService ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, instantiationService); } // we want a fixed size, so when we render to will measure our content and set that to be our diff --git a/src/sql/workbench/browser/modelComponents/diffeditor.component.ts b/src/sql/workbench/browser/modelComponents/diffeditor.component.ts index 465e52ab4d..d4906533c2 100644 --- a/src/sql/workbench/browser/modelComponents/diffeditor.component.ts +++ b/src/sql/workbench/browser/modelComponents/diffeditor.component.ts @@ -17,15 +17,15 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { ComponentBase } from 'sql/workbench/browser/modelComponents/componentBase'; import { IComponent, IComponentDescriptor, IModelStore } from 'sql/workbench/browser/modelComponents/interfaces'; -import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { SimpleEditorProgressService } from 'vs/editor/standalone/browser/simpleServices'; -import { IEditorProgressService } from 'vs/platform/progress/common/progress'; import { TextDiffEditor } from 'vs/workbench/browser/parts/editor/textDiffEditor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { TextDiffEditorModel } from 'vs/workbench/common/editor/textDiffEditorModel'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ITextModel } from 'vs/editor/common/model'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { SimpleProgressIndicator } from 'sql/workbench/services/progress/browser/simpleProgressIndicator'; +import { IEditorProgressService } from 'vs/platform/progress/common/progress'; @Component({ template: ` @@ -69,8 +69,8 @@ export default class DiffEditorComponent extends ComponentBase implements ICompo } private _createEditor(): void { - this._instantiationService = this._instantiationService.createChild(new ServiceCollection([IEditorProgressService, new SimpleEditorProgressService()])); - this._editor = this._instantiationService.createInstance(TextDiffEditor); + const customInstan = this._instantiationService.createChild(new ServiceCollection([IEditorProgressService, new SimpleProgressIndicator()])); + this._editor = customInstan.createInstance(TextDiffEditor); this._editor.reverseColoring(); this._editor.create(this._el.nativeElement); this._editor.setVisible(true); diff --git a/src/sql/workbench/browser/modelComponents/editor.component.ts b/src/sql/workbench/browser/modelComponents/editor.component.ts index c483f19f3e..e77d3496bf 100644 --- a/src/sql/workbench/browser/modelComponents/editor.component.ts +++ b/src/sql/workbench/browser/modelComponents/editor.component.ts @@ -19,11 +19,11 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { ComponentBase } from 'sql/workbench/browser/modelComponents/componentBase'; import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/workbench/browser/modelComponents/interfaces'; import { QueryTextEditor } from 'sql/workbench/browser/modelComponents/queryTextEditor'; -import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { SimpleEditorProgressService } from 'vs/editor/standalone/browser/simpleServices'; -import { IProgressService } from 'vs/platform/progress/common/progress'; import { ILogService } from 'vs/platform/log/common/log'; import { UntitledTextEditorInput } from 'vs/workbench/common/editor/untitledTextEditorInput'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { IEditorProgressService } from 'vs/platform/progress/common/progress'; +import { SimpleProgressIndicator } from 'sql/workbench/services/progress/browser/simpleProgressIndicator'; @Component({ template: '', @@ -61,12 +61,12 @@ export default class EditorComponent extends ComponentBase implements IComponent } private async _createEditor(): Promise { - let instantiationService = this._instantiationService.createChild(new ServiceCollection([IProgressService, new SimpleEditorProgressService()])); - this._editor = instantiationService.createInstance(QueryTextEditor); + const customInstan = this._instantiationService.createChild(new ServiceCollection([IEditorProgressService, new SimpleProgressIndicator()])); + this._editor = customInstan.createInstance(QueryTextEditor); this._editor.create(this._el.nativeElement); this._editor.setVisible(true); let uri = this.createUri(); - this._editorInput = instantiationService.createInstance(UntitledTextEditorInput, uri, false, 'plaintext', '', ''); + this._editorInput = this._instantiationService.createInstance(UntitledTextEditorInput, uri, false, 'plaintext', '', ''); await this._editor.setInput(this._editorInput, undefined); const model = await this._editorInput.resolve(); this._editorModel = model.textEditorModel; diff --git a/src/sql/workbench/browser/modelComponents/queryTextEditor.ts b/src/sql/workbench/browser/modelComponents/queryTextEditor.ts index 53ff98a927..686e010c78 100644 --- a/src/sql/workbench/browser/modelComponents/queryTextEditor.ts +++ b/src/sql/workbench/browser/modelComponents/queryTextEditor.ts @@ -16,13 +16,13 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; import { EditorOptions } from 'vs/workbench/common/editor'; -import { StandaloneCodeEditor } from 'vs/editor/standalone/browser/standaloneCodeEditor'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { UntitledTextEditorInput } from 'vs/workbench/common/editor/untitledTextEditorInput'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; /** * Extension of TextResourceEditor that is always readonly rather than only with non UntitledInputs @@ -56,7 +56,7 @@ export class QueryTextEditor extends BaseTextEditor { } public createEditorControl(parent: HTMLElement, configuration: IEditorOptions): editorCommon.IEditor { - return this.instantiationService.createInstance(StandaloneCodeEditor, parent, configuration); + return this.instantiationService.createInstance(CodeEditorWidget, parent, configuration, {}); } protected getConfigurationOverrides(): IEditorOptions { diff --git a/src/sql/workbench/browser/modelComponents/splitviewContainer.component.ts b/src/sql/workbench/browser/modelComponents/splitviewContainer.component.ts index 0f39698f9b..512f7fff8b 100644 --- a/src/sql/workbench/browser/modelComponents/splitviewContainer.component.ts +++ b/src/sql/workbench/browser/modelComponents/splitviewContainer.component.ts @@ -87,8 +87,8 @@ export default class SplitViewContainer extends ContainerBase im let c = component as ComponentBase; let basicView: SplitPane = new SplitPane(); basicView.orientation = orientation; - basicView.element = c.getHtml(), - basicView.component = c; + basicView.element = c.getHtml(); + basicView.component = c; basicView.minimumSize = 50; basicView.maximumSize = Number.MAX_VALUE; return basicView; diff --git a/src/sql/workbench/browser/parts/views/customView.ts b/src/sql/workbench/browser/parts/views/customView.ts index 7c9d0b105a..0a0244e70b 100644 --- a/src/sql/workbench/browser/parts/views/customView.ts +++ b/src/sql/workbench/browser/parts/views/customView.ts @@ -62,8 +62,9 @@ export class CustomTreeViewPanel extends ViewPane { @IContextMenuService contextMenuService: IContextMenuService, @IConfigurationService configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService, + @IInstantiationService instantiationService: IInstantiationService ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: options.title }, keybindingService, contextMenuService, configurationService, contextKeyService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: options.title }, keybindingService, contextMenuService, configurationService, contextKeyService, instantiationService); const { treeView } = (Registry.as(Extensions.ViewsRegistry).getView(options.id)); this.treeView = treeView as ITreeView; this._register(this.treeView.onDidChangeActions(() => this.updateActions(), this)); diff --git a/src/sql/workbench/common/workspaceActions.ts b/src/sql/workbench/common/workspaceActions.ts index 892bed6857..9df03e2393 100644 --- a/src/sql/workbench/common/workspaceActions.ts +++ b/src/sql/workbench/common/workspaceActions.ts @@ -5,7 +5,7 @@ import { Action } from 'vs/base/common/actions'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -//tslint:disable-next-line:layering +// eslint-disable-next-line code-layering import { IElectronService } from 'vs/platform/electron/node/electron'; import { URI } from 'vs/base/common/uri'; diff --git a/src/sql/workbench/contrib/accounts/browser/accountDialog.ts b/src/sql/workbench/contrib/accounts/browser/accountDialog.ts index f20b90112d..2d04717e21 100644 --- a/src/sql/workbench/contrib/accounts/browser/accountDialog.ts +++ b/src/sql/workbench/contrib/accounts/browser/accountDialog.ts @@ -46,11 +46,11 @@ class AccountPanel extends ViewPane { @IKeybindingService keybindingService: IKeybindingService, @IContextMenuService contextMenuService: IContextMenuService, @IConfigurationService configurationService: IConfigurationService, - @IInstantiationService private instantiationService: IInstantiationService, @IThemeService private themeService: IThemeService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @IInstantiationService instantiationService: IInstantiationService ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, instantiationService); } protected renderBody(container: HTMLElement): void { @@ -294,9 +294,9 @@ export class AccountDialog extends Modal { this._keybindingService, this._contextMenuService, this._configurationService, - this._instantiationService, this._themeService, - this.contextKeyService + this.contextKeyService, + this._instantiationService ); attachPanelStyler(providerView, this._themeService); diff --git a/src/sql/workbench/contrib/charts/browser/actions.ts b/src/sql/workbench/contrib/charts/browser/actions.ts index b2fda3a0c0..794e6201a3 100644 --- a/src/sql/workbench/contrib/charts/browser/actions.ts +++ b/src/sql/workbench/contrib/charts/browser/actions.ts @@ -72,7 +72,7 @@ export class CreateInsightAction extends Action { } }; - let input = this.untitledEditorService.createOrGet(undefined, 'json', JSON.stringify(widgetConfig)); + let input = this.untitledEditorService.create({ mode: 'json', initialValue: JSON.stringify(widgetConfig) }); return this.editorService.openEditor(input, { pinned: true }) .then( @@ -155,7 +155,7 @@ export class SaveImageAction extends Action { if (context.insight instanceof Graph) { let fileFilters = new Array({ extensions: ['png'], name: localize('resultsSerializer.saveAsFileExtensionPNGTitle', "PNG") }); - const filePath = await this.fileDialogService.pickFileToSave({ filters: fileFilters }); + const filePath = await this.fileDialogService.showSaveDialog({ filters: fileFilters }); const data = (context.insight).getCanvasData(); if (!data) { this.notificationService.error(localize('chartNotFound', "Could not find chart to save")); diff --git a/src/sql/workbench/contrib/charts/test/browser/chartView.test.ts b/src/sql/workbench/contrib/charts/test/browser/chartView.test.ts index 400a29272f..b8819a2ed9 100644 --- a/src/sql/workbench/contrib/charts/test/browser/chartView.test.ts +++ b/src/sql/workbench/contrib/charts/test/browser/chartView.test.ts @@ -10,7 +10,7 @@ import { TestLayoutService } from 'vs/workbench/test/workbenchTestServices'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { SimpleNotificationService } from 'vs/editor/standalone/browser/simpleServices'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; suite('Chart View', () => { test('initializes without error', () => { @@ -29,7 +29,7 @@ function createChartView(): ChartView { const contextViewService = new ContextViewService(layoutService); const themeService = new TestThemeService(); const instantiationService = new TestInstantiationService(); - const notificationService = new SimpleNotificationService(); + const notificationService = new TestNotificationService(); instantiationService.stub(IThemeService, themeService); return new ChartView(contextViewService, themeService, instantiationService, notificationService); } diff --git a/src/sql/workbench/contrib/commandLine/test/electron-browser/commandLine.test.ts b/src/sql/workbench/contrib/commandLine/test/electron-browser/commandLine.test.ts index b37011a62b..567e3b5323 100644 --- a/src/sql/workbench/contrib/commandLine/test/electron-browser/commandLine.test.ts +++ b/src/sql/workbench/contrib/commandLine/test/electron-browser/commandLine.test.ts @@ -34,7 +34,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { isUndefinedOrNull } from 'vs/base/common/types'; import { UntitledTextEditorInput } from 'vs/workbench/common/editor/untitledTextEditorInput'; -import { SimpleUriLabelService } from 'vs/editor/standalone/browser/simpleServices'; +import { LabelService } from 'vs/workbench/services/label/common/labelService'; class TestParsedArgs implements ParsedArgs { [arg: string]: any; @@ -393,7 +393,7 @@ suite('commandLineService tests', () => { querymodelService.setup(c => c.onRunQueryComplete).returns(() => Event.None); const instantiationService = new TestInstantiationService(); let uri = URI.file(args._[0]); - const untitledEditorInput = new UntitledTextEditorInput(uri, false, '', '', '', instantiationService, undefined, new SimpleUriLabelService(), undefined, undefined, undefined); + const untitledEditorInput = new UntitledTextEditorInput(uri, false, '', '', '', instantiationService, undefined, new LabelService(undefined, undefined), undefined, undefined, undefined); const queryInput = new UntitledQueryEditorInput(undefined, untitledEditorInput, undefined, connectionManagementService.object, querymodelService.object, configurationService.object, undefined); queryInput.state.connected = true; const editorService: TypeMoq.Mock = TypeMoq.Mock.ofType(TestEditorService, TypeMoq.MockBehavior.Strict); diff --git a/src/sql/workbench/contrib/dashboard/browser/containers/dashboardGridContainer.component.html b/src/sql/workbench/contrib/dashboard/browser/containers/dashboardGridContainer.component.html deleted file mode 100644 index 6240b6773d..0000000000 --- a/src/sql/workbench/contrib/dashboard/browser/containers/dashboardGridContainer.component.html +++ /dev/null @@ -1,33 +0,0 @@ - -
-
- - - - - - - - -
- - - - - - - - - - -
-
-
\ No newline at end of file diff --git a/src/sql/workbench/contrib/dashboard/browser/containers/dashboardGridContainer.component.ts b/src/sql/workbench/contrib/dashboard/browser/containers/dashboardGridContainer.component.ts index 0bc5c64158..ad091d6357 100644 --- a/src/sql/workbench/contrib/dashboard/browser/containers/dashboardGridContainer.component.ts +++ b/src/sql/workbench/contrib/dashboard/browser/containers/dashboardGridContainer.component.ts @@ -15,10 +15,10 @@ import { WebviewContent } from 'sql/workbench/contrib/dashboard/browser/contents import { TabChild } from 'sql/base/browser/ui/panel/tab.component'; import { Event, Emitter } from 'vs/base/common/event'; -import { ScrollbarVisibility } from 'vs/editor/common/standalone/standaloneEnums'; import { ScrollableDirective } from 'sql/base/browser/ui/scrollable/scrollable.directive'; import { values } from 'vs/base/common/collections'; import { fill } from 'vs/base/common/arrays'; +import { ScrollbarVisibility } from 'vs/base/common/scrollable'; export interface GridCellConfig { id?: string; @@ -46,7 +46,35 @@ export interface GridModelViewConfig extends GridCellConfig { @Component({ selector: 'dashboard-grid-container', - templateUrl: decodeURI(require.toUrl('./dashboardGridContainer.component.html')), + template: ` +
+
+ + + + + + + + +
+ + + + + + + + + + +
+
+
+ `, providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardGridContainer) }] }) export class DashboardGridContainer extends DashboardTab implements OnDestroy { @@ -56,7 +84,6 @@ export class DashboardGridContainer extends DashboardTab implements OnDestroy { public readonly onResize: Event = this._onResize.event; private cellWidth: number = 270; private cellHeight: number = 270; - public ScrollbarVisibility = ScrollbarVisibility; protected SKELETON_WIDTH = 5; diff --git a/src/sql/workbench/contrib/dashboard/browser/containers/dashboardHomeContainer.component.ts b/src/sql/workbench/contrib/dashboard/browser/containers/dashboardHomeContainer.component.ts index 17f3b6f00e..004e8ba661 100644 --- a/src/sql/workbench/contrib/dashboard/browser/containers/dashboardHomeContainer.component.ts +++ b/src/sql/workbench/contrib/dashboard/browser/containers/dashboardHomeContainer.component.ts @@ -17,14 +17,14 @@ import { ScrollableDirective } from 'sql/base/browser/ui/scrollable/scrollable.d import { TabChild } from 'sql/base/browser/ui/panel/tab.component'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ScrollbarVisibility } from 'vs/editor/common/standalone/standaloneEnums'; +import { ScrollbarVisibility } from 'vs/base/common/scrollable'; @Component({ selector: 'dashboard-home-container', providers: [{ provide: TabChild, useExisting: forwardRef(() => DashboardHomeContainer) }], template: `
-
+
@@ -39,8 +39,6 @@ export class DashboardHomeContainer extends DashboardWidgetContainer { @ViewChild('propertiesClass') private _propertiesClass: DashboardWidgetWrapper; @ContentChild(ScrollableDirective) private _scrollable: ScrollableDirective; - public ScrollbarVisibility = ScrollbarVisibility; - constructor( @Inject(forwardRef(() => ChangeDetectorRef)) _cd: ChangeDetectorRef, @Inject(forwardRef(() => CommonServiceInterface)) protected dashboardService: DashboardServiceInterface, diff --git a/src/sql/workbench/contrib/dataExplorer/browser/connectionViewletPanel.ts b/src/sql/workbench/contrib/dataExplorer/browser/connectionViewletPanel.ts index 04332b98ea..258b30ee06 100644 --- a/src/sql/workbench/contrib/dataExplorer/browser/connectionViewletPanel.ts +++ b/src/sql/workbench/contrib/dataExplorer/browser/connectionViewletPanel.ts @@ -35,12 +35,12 @@ export class ConnectionViewletPanel extends ViewPane { private options: IViewletViewOptions, @IKeybindingService keybindingService: IKeybindingService, @IContextMenuService contextMenuService: IContextMenuService, - @IInstantiationService private readonly instantiationService: IInstantiationService, + @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, @IObjectExplorerService private readonly objectExplorerService: IObjectExplorerService, @IContextKeyService contextKeyService: IContextKeyService ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: options.title }, keybindingService, contextMenuService, configurationService, contextKeyService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: options.title }, keybindingService, contextMenuService, configurationService, contextKeyService, instantiationService); this._addServerAction = this.instantiationService.createInstance(AddServerAction, AddServerAction.ID, AddServerAction.LABEL); diff --git a/src/sql/workbench/contrib/dataExplorer/browser/dataExplorerExtensionPoint.ts b/src/sql/workbench/contrib/dataExplorer/browser/dataExplorerExtensionPoint.ts index fd68f723c3..bd3108f609 100644 --- a/src/sql/workbench/contrib/dataExplorer/browser/dataExplorerExtensionPoint.ts +++ b/src/sql/workbench/contrib/dataExplorer/browser/dataExplorerExtensionPoint.ts @@ -16,6 +16,7 @@ import { coalesce } from 'vs/base/common/arrays'; import { CustomTreeViewPanel, CustomTreeView } from 'sql/workbench/browser/parts/views/customView'; import { VIEWLET_ID } from 'sql/workbench/contrib/dataExplorer/browser/dataExplorerViewlet'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; interface IUserFriendlyViewDescriptor { id: string; @@ -105,11 +106,13 @@ export class DataExplorerContainerExtensionHandler implements IWorkbenchContribu const viewDescriptor = { id: item.id, name: item.name, - ctorDescriptor: { ctor: CustomTreeViewPanel }, + ctorDescriptor: new SyncDescriptor(CustomTreeViewPanel), when: ContextKeyExpr.deserialize(item.when), canToggleVisibility: true, collapsed: this.showCollapsed(container), - treeView: this.instantiationService.createInstance(CustomTreeView, item.id, item.name, container) + treeView: this.instantiationService.createInstance(CustomTreeView, item.id, item.name, container), + extensionId: extension.description.identifier, + originalContainerId: entry.key }; viewIds.push(viewDescriptor.id); @@ -129,15 +132,15 @@ export class DataExplorerContainerExtensionHandler implements IWorkbenchContribu for (let descriptor of viewDescriptors) { if (typeof descriptor.id !== 'string') { - collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", "id")); + collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'id')); return false; } if (typeof descriptor.name !== 'string') { - collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", "name")); + collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'name')); return false; } if (descriptor.when && typeof descriptor.when !== 'string') { - collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", "when")); + collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'when')); return false; } } diff --git a/src/sql/workbench/contrib/dataExplorer/browser/dataExplorerViewlet.ts b/src/sql/workbench/contrib/dataExplorer/browser/dataExplorerViewlet.ts index 3b8f07449c..e195c848dc 100644 --- a/src/sql/workbench/contrib/dataExplorer/browser/dataExplorerViewlet.ts +++ b/src/sql/workbench/contrib/dataExplorer/browser/dataExplorerViewlet.ts @@ -27,6 +27,7 @@ import { ShowViewletAction, Viewlet } from 'vs/workbench/browser/viewlet'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { ViewPaneContainer, ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; export const VIEWLET_ID = 'workbench.view.connections'; @@ -62,7 +63,7 @@ export class DataExplorerViewletViewsContribution implements IWorkbenchContribut return { id: ConnectionViewletPanel.ID, name: localize('dataExplorer.servers', "Servers"), - ctorDescriptor: { ctor: ConnectionViewletPanel }, + ctorDescriptor: new SyncDescriptor(ConnectionViewletPanel), weight: 100, canToggleVisibility: true, order: 0 @@ -159,6 +160,6 @@ export class DataExplorerViewPaneContainer extends ViewPaneContainer { export const VIEW_CONTAINER = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ id: VIEWLET_ID, name: localize('dataexplorer.name', "Connections"), - ctorDescriptor: { ctor: DataExplorerViewPaneContainer }, + ctorDescriptor: new SyncDescriptor(DataExplorerViewPaneContainer), icon: 'dataExplorer' }, ViewContainerLocation.Sidebar); diff --git a/src/sql/workbench/contrib/editData/browser/editDataInput.ts b/src/sql/workbench/contrib/editData/browser/editDataInput.ts index b71b3b3c55..12c41764ab 100644 --- a/src/sql/workbench/contrib/editData/browser/editDataInput.ts +++ b/src/sql/workbench/contrib/editData/browser/editDataInput.ts @@ -217,7 +217,6 @@ export class EditDataInput extends EditorInput implements IConnectableInput { return this._connectionManagementService.getTabColorForUri(this.uri); } - public get onDidModelChangeContent(): Event { return this._sql.onDidModelChangeContent; } public get onDidModelChangeEncoding(): Event { return this._sql.onDidModelChangeEncoding; } public resolve(refresh?: boolean): Promise { return this._sql.resolve(); } public getEncoding(): string { return this._sql.getEncoding(); } diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts b/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts index d78fb76c18..5ab03f5a4c 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts @@ -15,9 +15,6 @@ import { NotebookModel } from 'sql/workbench/contrib/notebook/browser/models/not import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import * as themeColors from 'vs/workbench/common/theme'; -import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { SimpleEditorProgressService } from 'vs/editor/standalone/browser/simpleServices'; -import { IProgressService } from 'vs/platform/progress/common/progress'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITextModel } from 'vs/editor/common/model'; import * as DOM from 'vs/base/browser/dom'; @@ -35,6 +32,9 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { CellView } from 'sql/workbench/contrib/notebook/browser/cellViews/interfaces'; import { UntitledTextEditorInput } from 'vs/workbench/common/editor/untitledTextEditorInput'; import { UntitledTextEditorModel } from 'vs/workbench/common/editor/untitledTextEditorModel'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { IEditorProgressService } from 'vs/platform/progress/common/progress'; +import { SimpleProgressIndicator } from 'sql/workbench/services/progress/browser/simpleProgressIndicator'; export const CODE_SELECTOR: string = 'code-component'; const MARKDOWN_CLASS = 'markdown'; @@ -200,8 +200,8 @@ export class CodeComponent extends CellView implements OnInit, OnChanges { } private async createEditor(): Promise { - let instantiationService = this._instantiationService.createChild(new ServiceCollection([IProgressService, new SimpleEditorProgressService()])); - this._editor = instantiationService.createInstance(QueryTextEditor); + const customInstan = this._instantiationService.createChild(new ServiceCollection([IEditorProgressService, new SimpleProgressIndicator()])); + this._editor = customInstan.createInstance(QueryTextEditor); this._editor.create(this.codeElement.nativeElement); this._editor.setVisible(true); this._editor.setMinimumHeight(this._minimumHeight); @@ -210,7 +210,7 @@ export class CodeComponent extends CellView implements OnInit, OnChanges { let uri = this.cellModel.cellUri; let cellModelSource: string; cellModelSource = Array.isArray(this.cellModel.source) ? this.cellModel.source.join('') : this.cellModel.source; - this._editorInput = instantiationService.createInstance(UntitledTextEditorInput, uri, false, this.cellModel.language, cellModelSource, ''); + this._editorInput = this._instantiationService.createInstance(UntitledTextEditorInput, uri, false, this.cellModel.language, cellModelSource, ''); await this._editor.setInput(this._editorInput, undefined); this.setFocusAndScroll(); diff --git a/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts b/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts index dd47cc63bd..119796e0ad 100644 --- a/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts +++ b/src/sql/workbench/contrib/notebook/browser/models/notebookInput.ts @@ -16,7 +16,7 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { INotebookModel, IContentManager, NotebookContentChange } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { Schemas } from 'vs/base/common/network'; -import { StateChange, ITextFileSaveOptions } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileSaveOptions } from 'vs/workbench/services/textfile/common/textfiles'; import { LocalContentManager } from 'sql/workbench/services/notebook/common/localContentManager'; import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -73,12 +73,14 @@ export class NotebookEditorModel extends EditorModel { })); } else { if (this.textEditorModel instanceof TextFileEditorModel) { - this._register(this.textEditorModel.onDidStateChange(change => { + this._register(this.textEditorModel.onDidSave(() => { + let dirty = this.textEditorModel instanceof ResourceEditorModel ? false : this.textEditorModel.isDirty(); + this.setDirty(dirty); + this.sendNotebookSerializationStateChange(); + })); + this._register(this.textEditorModel.onDidChangeDirty(() => { let dirty = this.textEditorModel instanceof ResourceEditorModel ? false : this.textEditorModel.isDirty(); this.setDirty(dirty); - if (change === StateChange.SAVED) { - this.sendNotebookSerializationStateChange(); - } })); } } diff --git a/src/sql/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/sql/workbench/contrib/notebook/browser/notebook.contribution.ts index 8ff9e44d56..7e2cafef8d 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -14,12 +14,11 @@ import { UntitledNotebookInput } from 'sql/workbench/contrib/notebook/common/mod import { FileNotebookInput } from 'sql/workbench/contrib/notebook/common/models/fileNotebookInput'; import { FileNoteBookEditorInputFactory, UntitledNoteBookEditorInputFactory, NotebookEditorInputAssociation } from 'sql/workbench/contrib/notebook/common/models/nodebookInputFactory'; import { IWorkbenchActionRegistry, Extensions as WorkbenchActionsExtensions } from 'vs/workbench/common/actions'; -import { SyncActionDescriptor, registerAction, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, registerAction2, MenuRegistry, MenuId, Action2 } from 'vs/platform/actions/common/actions'; import { NotebookEditor } from 'sql/workbench/contrib/notebook/browser/notebookEditor'; import { NewNotebookAction } from 'sql/workbench/contrib/notebook/browser/notebookActions'; -import { KeyMod } from 'vs/editor/common/standalone/standaloneBase'; -import { KeyCode } from 'vs/base/common/keyCodes'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { IConfigurationRegistry, Extensions as ConfigExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { GridOutputComponent } from 'sql/workbench/contrib/notebook/browser/outputs/gridOutput.component'; import { PlotlyOutputComponent } from 'sql/workbench/contrib/notebook/browser/outputs/plotlyOutput.component'; @@ -131,9 +130,15 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerWidgetContext, { order: 1 }); -registerAction({ - id: 'workbench.action.setWorkspaceAndOpen', - handler: async (accessor, options: { forceNewWindow: boolean, folderPath: URI }) => { +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.setWorkspaceAndOpen', + title: localize('workbench.action.setWorkspaceAndOpen', "Set Workspace And Open") + }); + } + + run = async (accessor, options: { forceNewWindow: boolean, folderPath: URI }) => { const viewletService = accessor.get(IViewletService); const workspaceEditingService = accessor.get(IWorkspaceEditingService); const hostService = accessor.get(IHostService); @@ -150,7 +155,7 @@ registerAction({ else { return hostService.reload(); } - } + }; }); const configurationRegistry = Registry.as(ConfigExtensions.Configuration); @@ -167,14 +172,6 @@ configurationRegistry.registerConfiguration({ } }); -registerAction({ - id: 'workbench.books.action.focusBooksExplorer', - handler: async (accessor) => { - const viewletService = accessor.get(IViewletService); - viewletService.openViewlet('workbench.view.extension.books-explorer', true); - } -}); - /* *************** Output components *************** */ // Note: most existing types use the same component to render. In order to // preserve correct rank order, we register it once for each different rank of diff --git a/src/sql/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts b/src/sql/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts index 9b75fad0de..172ffe2a09 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts @@ -142,8 +142,8 @@ suite('Notebook Editor Model', function (): void { }); teardown(() => { - if (accessor && accessor.textFileService && accessor.textFileService.models) { - (accessor.textFileService.models).clear(); + if (accessor && accessor.textFileService && accessor.textFileService.files) { + (accessor.textFileService.files).clear(); } }); @@ -870,7 +870,7 @@ suite('Notebook Editor Model', function (): void { async function createTextEditorModel(self: Mocha.ITestCallbackContext): Promise { let textFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(self, defaultUri.toString()), 'utf8', undefined); - (accessor.textFileService.models).add(textFileEditorModel.resource, textFileEditorModel); + (accessor.textFileService.files).add(textFileEditorModel.resource, textFileEditorModel); await textFileEditorModel.load(); return new NotebookEditorModel(defaultUri, textFileEditorModel, mockNotebookService.object, testResourcePropertiesService); } diff --git a/src/sql/workbench/contrib/notebook/test/browser/notebookInput.test.ts b/src/sql/workbench/contrib/notebook/test/browser/notebookInput.test.ts index 29903a0b45..17f4513a1e 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/notebookInput.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/notebookInput.test.ts @@ -16,10 +16,10 @@ import { UntitledTextEditorModel } from 'vs/workbench/common/editor/untitledText import { NodeStub, NotebookServiceStub } from 'sql/workbench/contrib/notebook/test/stubs'; import { basenameOrAuthority } from 'vs/base/common/resources'; import { UntitledTextEditorInput } from 'vs/workbench/common/editor/untitledTextEditorInput'; -import { SimpleUriLabelService } from 'vs/editor/standalone/browser/simpleServices'; import { IExtensionService, NullExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { INotebookService, IProviderInfo } from 'sql/workbench/services/notebook/browser/notebookService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { LabelService } from 'vs/workbench/services/label/common/labelService'; suite('Notebook Input', function (): void { const instantiationService = workbenchInstantiationService(); @@ -48,7 +48,7 @@ suite('Notebook Input', function (): void { let untitledNotebookInput: UntitledNotebookInput; setup(() => { - untitledTextInput = new UntitledTextEditorInput(untitledUri, false, '', '', '', instantiationService, undefined, new SimpleUriLabelService(), undefined, undefined, undefined); + untitledTextInput = new UntitledTextEditorInput(untitledUri, false, '', '', '', instantiationService, undefined, new LabelService(undefined, undefined), undefined, undefined, undefined); untitledNotebookInput = new UntitledNotebookInput( testTitle, untitledUri, untitledTextInput, undefined, instantiationService, mockNotebookService.object, mockExtensionService.object); @@ -169,7 +169,7 @@ suite('Notebook Input', function (): void { assert.ok(untitledNotebookInput.matches(untitledNotebookInput), 'Input should match itself.'); let otherTestUri = URI.from({ scheme: Schemas.untitled, path: 'OtherTestPath' }); - let otherTextInput = new UntitledTextEditorInput(otherTestUri, false, '', '', '', instantiationService, undefined, new SimpleUriLabelService(), undefined, undefined, undefined); + let otherTextInput = new UntitledTextEditorInput(otherTestUri, false, '', '', '', instantiationService, undefined, new LabelService(undefined, undefined), undefined, undefined, undefined); let otherInput = instantiationService.createInstance(UntitledNotebookInput, 'OtherTestInput', otherTestUri, otherTextInput); assert.strictEqual(untitledNotebookInput.matches(otherInput), false, 'Input should not match different input.'); diff --git a/src/sql/workbench/contrib/notebook/test/stubs.ts b/src/sql/workbench/contrib/notebook/test/stubs.ts index 5751fd0c47..1e2fd09d58 100644 --- a/src/sql/workbench/contrib/notebook/test/stubs.ts +++ b/src/sql/workbench/contrib/notebook/test/stubs.ts @@ -516,7 +516,7 @@ export class NodeStub implements Node { get parentNode(): Node & ParentNode { throw new Error('Method not implemented.'); } - get previousSibling(): Node { + get previousSibling(): ChildNode { throw new Error('Method not implemented.'); } nodeValue: string; diff --git a/src/sql/workbench/contrib/profiler/browser/profilerResourceEditor.ts b/src/sql/workbench/contrib/profiler/browser/profilerResourceEditor.ts index 8e246a3b6d..31677b008c 100644 --- a/src/sql/workbench/contrib/profiler/browser/profilerResourceEditor.ts +++ b/src/sql/workbench/contrib/profiler/browser/profilerResourceEditor.ts @@ -16,13 +16,13 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; import { EditorOptions } from 'vs/workbench/common/editor'; -import { StandaloneCodeEditor } from 'vs/editor/standalone/browser/standaloneCodeEditor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { UntitledTextEditorInput } from 'vs/workbench/common/editor/untitledTextEditorInput'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; -class ProfilerResourceCodeEditor extends StandaloneCodeEditor { +class ProfilerResourceCodeEditor extends CodeEditorWidget { // protected _getContributions(): IEditorContributionCtor[] { // let contributions = super._getContributions(); @@ -53,7 +53,7 @@ export class ProfilerResourceEditor extends BaseTextEditor { } public createEditorControl(parent: HTMLElement, configuration: IEditorOptions): editorCommon.IEditor { - return this.instantiationService.createInstance(ProfilerResourceCodeEditor, parent, configuration); + return this.instantiationService.createInstance(ProfilerResourceCodeEditor, parent, configuration, {}); } protected getConfigurationOverrides(): IEditorOptions { diff --git a/src/sql/workbench/contrib/query/browser/gridPanel.ts b/src/sql/workbench/contrib/query/browser/gridPanel.ts index f6357105ea..5c79c9f53c 100644 --- a/src/sql/workbench/contrib/query/browser/gridPanel.ts +++ b/src/sql/workbench/contrib/query/browser/gridPanel.ts @@ -579,7 +579,7 @@ export abstract class GridTableBase extends Disposable implements IView { let value = d.resultSubset.rows[0][event.cell.cell - 1]; let content = value.displayValue; - const input = this.untitledEditorService.createOrGet(undefined, column.isXml ? 'xml' : 'json', content); + const input = this.untitledEditorService.create({ mode: column.isXml ? 'xml' : 'json', initialValue: content }); const model = await input.resolve(); await this.instantiationService.invokeFunction(formatDocumentWithSelectedProvider, model.textEditorModel, FormattingMode.Explicit, CancellationToken.None); return this.editorService.openEditor(input); diff --git a/src/sql/workbench/contrib/query/common/untitledQueryEditorInput.ts b/src/sql/workbench/contrib/query/common/untitledQueryEditorInput.ts index 8b50cb78e3..ae379e36e7 100644 --- a/src/sql/workbench/contrib/query/common/untitledQueryEditorInput.ts +++ b/src/sql/workbench/contrib/query/common/untitledQueryEditorInput.ts @@ -21,7 +21,6 @@ export class UntitledQueryEditorInput extends QueryEditorInput implements IEncod public static readonly ID = 'workbench.editorInput.untitledQueryInput'; - public readonly onDidModelChangeContent = this.text.onDidModelChangeContent; public readonly onDidModelChangeEncoding = this.text.onDidModelChangeEncoding; constructor( diff --git a/src/sql/workbench/contrib/query/test/browser/queryActions.test.ts b/src/sql/workbench/contrib/query/test/browser/queryActions.test.ts index d2a1f83e68..754c657ff0 100644 --- a/src/sql/workbench/contrib/query/test/browser/queryActions.test.ts +++ b/src/sql/workbench/contrib/query/test/browser/queryActions.test.ts @@ -33,7 +33,7 @@ import { TestInstantiationService } from 'vs/platform/instantiation/test/common/ import { TestConnectionManagementService } from 'sql/platform/connection/test/common/testConnectionManagementService'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { UntitledTextEditorInput } from 'vs/workbench/common/editor/untitledTextEditorInput'; -import { SimpleUriLabelService } from 'vs/editor/standalone/browser/simpleServices'; +import { LabelService } from 'vs/workbench/services/label/common/labelService'; suite('SQL QueryAction Tests', () => { @@ -70,7 +70,7 @@ suite('SQL QueryAction Tests', () => { connectionManagementService = TypeMoq.Mock.ofType(TestConnectionManagementService); connectionManagementService.setup(q => q.onDisconnect).returns(() => Event.None); const instantiationService = new TestInstantiationService(); - let fileInput = new UntitledTextEditorInput(URI.parse('file://testUri'), false, '', '', '', instantiationService, undefined, new SimpleUriLabelService(), undefined, undefined, undefined); + let fileInput = new UntitledTextEditorInput(URI.parse('file://testUri'), false, '', '', '', instantiationService, undefined, new LabelService(undefined, undefined), undefined, undefined, undefined); // Setup a reusable mock QueryInput testQueryInput = TypeMoq.Mock.ofType(UntitledQueryEditorInput, TypeMoq.MockBehavior.Strict, undefined, fileInput, undefined, connectionManagementService.object, queryModelService.object, configurationService.object); testQueryInput.setup(x => x.uri).returns(() => testUri); @@ -175,7 +175,7 @@ suite('SQL QueryAction Tests', () => { queryModelService.setup(x => x.onRunQueryStart).returns(() => Event.None); queryModelService.setup(x => x.onRunQueryComplete).returns(() => Event.None); const instantiationService = new TestInstantiationService(); - let fileInput = new UntitledTextEditorInput(URI.parse('file://testUri'), false, '', '', '', instantiationService, undefined, new SimpleUriLabelService(), undefined, undefined, undefined); + let fileInput = new UntitledTextEditorInput(URI.parse('file://testUri'), false, '', '', '', instantiationService, undefined, new LabelService(undefined, undefined), undefined, undefined, undefined); // ... Mock "isSelectionEmpty" in QueryEditor let queryInput = TypeMoq.Mock.ofType(UntitledQueryEditorInput, TypeMoq.MockBehavior.Strict, undefined, fileInput, undefined, connectionManagementService.object, queryModelService.object, configurationService.object); @@ -224,7 +224,7 @@ suite('SQL QueryAction Tests', () => { // ... Mock "getSelection" in QueryEditor const instantiationService = new TestInstantiationService(); - let fileInput = new UntitledTextEditorInput(URI.parse('file://testUri'), false, '', '', '', instantiationService, undefined, new SimpleUriLabelService(), undefined, undefined, undefined); + let fileInput = new UntitledTextEditorInput(URI.parse('file://testUri'), false, '', '', '', instantiationService, undefined, new LabelService(undefined, undefined), undefined, undefined, undefined); let queryInput = TypeMoq.Mock.ofType(UntitledQueryEditorInput, TypeMoq.MockBehavior.Loose, undefined, fileInput, undefined, connectionManagementService.object, queryModelService.object, configurationService.object); queryInput.setup(x => x.uri).returns(() => testUri); diff --git a/src/sql/workbench/contrib/query/test/browser/queryEditor.test.ts b/src/sql/workbench/contrib/query/test/browser/queryEditor.test.ts index 2e9c9f6236..eb92c8b9d6 100644 --- a/src/sql/workbench/contrib/query/test/browser/queryEditor.test.ts +++ b/src/sql/workbench/contrib/query/test/browser/queryEditor.test.ts @@ -24,7 +24,7 @@ import { UntitledTextEditorInput } from 'vs/workbench/common/editor/untitledText import { UntitledQueryEditorInput } from 'sql/workbench/contrib/query/common/untitledQueryEditorInput'; import { TestQueryModelService } from 'sql/platform/query/test/common/testQueryModelService'; import { Event } from 'vs/base/common/event'; -import { SimpleUriLabelService } from 'vs/editor/standalone/browser/simpleServices'; +import { LabelService } from 'vs/workbench/services/label/common/labelService'; suite('SQL QueryEditor Tests', () => { let instantiationService: TypeMoq.Mock; @@ -285,7 +285,7 @@ suite('SQL QueryEditor Tests', () => { return new RunQueryAction(undefined, undefined, undefined); }); - let fileInput = new UntitledTextEditorInput(URI.parse('file://testUri'), false, '', '', '', instantiationService.object, undefined, new SimpleUriLabelService(), undefined, undefined, undefined); + let fileInput = new UntitledTextEditorInput(URI.parse('file://testUri'), false, '', '', '', instantiationService.object, undefined, new LabelService(undefined, undefined), undefined, undefined, undefined); queryModelService = TypeMoq.Mock.ofType(TestQueryModelService, TypeMoq.MockBehavior.Strict); queryModelService.setup(x => x.disposeQuery(TypeMoq.It.isAny())); queryModelService.setup(x => x.onRunQueryComplete).returns(() => Event.None); diff --git a/src/sql/workbench/contrib/queryPlan/common/planXmlParser.ts b/src/sql/workbench/contrib/queryPlan/browser/planXmlParser.ts similarity index 100% rename from src/sql/workbench/contrib/queryPlan/common/planXmlParser.ts rename to src/sql/workbench/contrib/queryPlan/browser/planXmlParser.ts diff --git a/src/sql/workbench/contrib/queryPlan/browser/topOperations.ts b/src/sql/workbench/contrib/queryPlan/browser/topOperations.ts index 30c41a6e5d..e2ce018311 100644 --- a/src/sql/workbench/contrib/queryPlan/browser/topOperations.ts +++ b/src/sql/workbench/contrib/queryPlan/browser/topOperations.ts @@ -8,7 +8,7 @@ import { localize } from 'vs/nls'; import { Disposable } from 'vs/base/common/lifecycle'; import { Table } from 'sql/base/browser/ui/table/table'; -import { PlanXmlParser } from 'sql/workbench/contrib/queryPlan/common/planXmlParser'; +import { PlanXmlParser } from 'sql/workbench/contrib/queryPlan/browser/planXmlParser'; import { IPanelView, IPanelTab } from 'sql/base/browser/ui/panel/panel'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { attachTableStyler } from 'sql/platform/theme/common/styler'; diff --git a/src/sql/workbench/services/insights/browser/insightsDialogView.ts b/src/sql/workbench/services/insights/browser/insightsDialogView.ts index 0fe73bd50d..d0f6c9d136 100644 --- a/src/sql/workbench/services/insights/browser/insightsDialogView.ts +++ b/src/sql/workbench/services/insights/browser/insightsDialogView.ts @@ -62,9 +62,10 @@ class InsightTableView extends ViewPane { @IKeybindingService keybindingService: IKeybindingService, @IContextMenuService contextMenuService: IContextMenuService, @IConfigurationService configurationService: IConfigurationService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @IInstantiationService instantiationService: IInstantiationService ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, instantiationService); } protected renderBody(container: HTMLElement): void { diff --git a/src/sql/workbench/services/progress/browser/simpleProgressIndicator.ts b/src/sql/workbench/services/progress/browser/simpleProgressIndicator.ts new file mode 100644 index 0000000000..c4dba3dd7b --- /dev/null +++ b/src/sql/workbench/services/progress/browser/simpleProgressIndicator.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IProgressIndicator, IProgressRunner } from 'vs/platform/progress/common/progress'; + +export class SimpleProgressIndicator implements IProgressIndicator { + show(infinite: true, delay?: number): IProgressRunner; + show(total: number, delay?: number): IProgressRunner; + show(total: any, delay?: any) { + return { + total: (total: number) => { + }, + + worked: (worked: number) => { + }, + + done: () => { + } + }; + } + + async showWhile(promise: Promise, delay?: number): Promise { + try { + await promise; + } catch (e) { + + } + } + + _serviceBrand: undefined; +} diff --git a/src/sql/workbench/services/queryEditor/browser/queryEditorService.ts b/src/sql/workbench/services/queryEditor/browser/queryEditorService.ts index 9384208d39..9ebfbc7a85 100644 --- a/src/sql/workbench/services/queryEditor/browser/queryEditorService.ts +++ b/src/sql/workbench/services/queryEditor/browser/queryEditorService.ts @@ -52,7 +52,7 @@ export class QueryEditorService implements IQueryEditorService { let docUri: URI = URI.from({ scheme: Schemas.untitled, path: filePath }); // Create a sql document pane with accoutrements - const fileInput = this._untitledEditorService.createOrGet(docUri, 'sql'); + const fileInput = this._untitledEditorService.create({ associatedResource: docUri, mode: 'sql' }); let untitledEditorModel = await fileInput.resolve(); if (sqlContent) { untitledEditorModel.textEditorModel.setValue(sqlContent); @@ -87,7 +87,7 @@ export class QueryEditorService implements IQueryEditorService { let docUri: URI = URI.from({ scheme: Schemas.untitled, path: filePath }); // Create a sql document pane with accoutrements - const fileInput = this._untitledEditorService.createOrGet(docUri, 'sql'); + const fileInput = this._untitledEditorService.create({ associatedResource: docUri, mode: 'sql' }); const m = await fileInput.resolve(); if (sqlContent) { m.textEditorModel.setValue(sqlContent); diff --git a/src/tsconfig.json b/src/tsconfig.json index e574faad00..20ea16c2df 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -25,10 +25,6 @@ "./sql" ], "exclude": [ - "./typings/es6-promise.d.ts", - "./typings/require-monaco.d.ts", - "./typings/xterm.d.ts", - "./typings/xterm-addon-search.d.ts", - "./typings/xterm-addon-web-links.d.ts" + "./typings/es6-promise.d.ts" ] } diff --git a/src/typings/lib.ie11_safe_es6.d.ts b/src/typings/lib.ie11_safe_es6.d.ts index f29d340dc0..548703cfc0 100644 --- a/src/typings/lib.ie11_safe_es6.d.ts +++ b/src/typings/lib.ie11_safe_es6.d.ts @@ -12,7 +12,7 @@ interface Map { forEach(callbackfn: (value: V, index: K, map: Map) => void, thisArg?: any): void; get(key: K): V | undefined; has(key: K): boolean; - set(key: K, value?: V): Map; + set(key: K, value: V): Map; readonly size: number; // not supported on IE11: diff --git a/src/typings/require.d.ts b/src/typings/require.d.ts index b11fa0b660..7cfd14cef0 100644 --- a/src/typings/require.d.ts +++ b/src/typings/require.d.ts @@ -31,7 +31,7 @@ declare class LoaderEvent { readonly detail: string; } -declare var define: { +declare const define: { (moduleName: string, dependencies: string[], callback: (...args: any[]) => any): any; (moduleName: string, dependencies: string[], definition: any): any; (moduleName: string, callback: (...args: any[]) => any): any; diff --git a/src/vs/base/browser/globalMouseMoveMonitor.ts b/src/vs/base/browser/globalMouseMoveMonitor.ts index 98a5274b19..c50e6e71ff 100644 --- a/src/vs/base/browser/globalMouseMoveMonitor.ts +++ b/src/vs/base/browser/globalMouseMoveMonitor.ts @@ -12,6 +12,7 @@ import { BrowserFeatures } from 'vs/base/browser/canIUse'; export interface IStandardMouseMoveEventData { leftButton: boolean; + buttons: number; posx: number; posy: number; } @@ -33,21 +34,22 @@ export function standardMouseMoveMerger(lastEvent: IStandardMouseMoveEventData | ev.preventDefault(); return { leftButton: ev.leftButton, + buttons: ev.buttons, posx: ev.posx, posy: ev.posy }; } -export class GlobalMouseMoveMonitor implements IDisposable { +export class GlobalMouseMoveMonitor implements IDisposable { - protected readonly hooks = new DisposableStore(); - protected mouseMoveEventMerger: IEventMerger | null = null; - protected mouseMoveCallback: IMouseMoveCallback | null = null; - protected onStopCallback: IOnStopCallback | null = null; + private readonly _hooks = new DisposableStore(); + private _mouseMoveEventMerger: IEventMerger | null = null; + private _mouseMoveCallback: IMouseMoveCallback | null = null; + private _onStopCallback: IOnStopCallback | null = null; public dispose(): void { this.stopMonitoring(false); - this.hooks.dispose(); + this._hooks.dispose(); } public stopMonitoring(invokeStopCallback: boolean): void { @@ -57,11 +59,11 @@ export class GlobalMouseMoveMonitor implements IDisposable { } // Unhook - this.hooks.clear(); - this.mouseMoveEventMerger = null; - this.mouseMoveCallback = null; - const onStopCallback = this.onStopCallback; - this.onStopCallback = null; + this._hooks.clear(); + this._mouseMoveEventMerger = null; + this._mouseMoveCallback = null; + const onStopCallback = this._onStopCallback; + this._onStopCallback = null; if (invokeStopCallback && onStopCallback) { onStopCallback(); @@ -69,10 +71,11 @@ export class GlobalMouseMoveMonitor implements IDisposable { } public isMonitoring(): boolean { - return !!this.mouseMoveEventMerger; + return !!this._mouseMoveEventMerger; } public startMonitoring( + initialButtons: number, mouseMoveEventMerger: IEventMerger, mouseMoveCallback: IMouseMoveCallback, onStopCallback: IOnStopCallback @@ -81,40 +84,47 @@ export class GlobalMouseMoveMonitor implements IDisposable { // I am already hooked return; } - this.mouseMoveEventMerger = mouseMoveEventMerger; - this.mouseMoveCallback = mouseMoveCallback; - this.onStopCallback = onStopCallback; + this._mouseMoveEventMerger = mouseMoveEventMerger; + this._mouseMoveCallback = mouseMoveCallback; + this._onStopCallback = onStopCallback; let windowChain = IframeUtils.getSameOriginWindowChain(); const mouseMove = platform.isIOS && BrowserFeatures.pointerEvents ? 'pointermove' : 'mousemove'; const mouseUp = platform.isIOS && BrowserFeatures.pointerEvents ? 'pointerup' : 'mouseup'; for (const element of windowChain) { - this.hooks.add(dom.addDisposableThrottledListener(element.window.document, mouseMove, - (data: R) => this.mouseMoveCallback!(data), - (lastEvent: R | null, currentEvent) => this.mouseMoveEventMerger!(lastEvent, currentEvent as MouseEvent) + this._hooks.add(dom.addDisposableThrottledListener(element.window.document, mouseMove, + (data: R) => { + if (data.buttons !== initialButtons) { + // Buttons state has changed in the meantime + this.stopMonitoring(true); + return; + } + this._mouseMoveCallback!(data); + }, + (lastEvent: R | null, currentEvent) => this._mouseMoveEventMerger!(lastEvent, currentEvent as MouseEvent) )); - this.hooks.add(dom.addDisposableListener(element.window.document, mouseUp, (e: MouseEvent) => this.stopMonitoring(true))); + this._hooks.add(dom.addDisposableListener(element.window.document, mouseUp, (e: MouseEvent) => this.stopMonitoring(true))); } if (IframeUtils.hasDifferentOriginAncestor()) { let lastSameOriginAncestor = windowChain[windowChain.length - 1]; // We might miss a mouse up if it happens outside the iframe // This one is for Chrome - this.hooks.add(dom.addDisposableListener(lastSameOriginAncestor.window.document, 'mouseout', (browserEvent: MouseEvent) => { + this._hooks.add(dom.addDisposableListener(lastSameOriginAncestor.window.document, 'mouseout', (browserEvent: MouseEvent) => { let e = new StandardMouseEvent(browserEvent); if (e.target.tagName.toLowerCase() === 'html') { this.stopMonitoring(true); } })); // This one is for FF - this.hooks.add(dom.addDisposableListener(lastSameOriginAncestor.window.document, 'mouseover', (browserEvent: MouseEvent) => { + this._hooks.add(dom.addDisposableListener(lastSameOriginAncestor.window.document, 'mouseover', (browserEvent: MouseEvent) => { let e = new StandardMouseEvent(browserEvent); if (e.target.tagName.toLowerCase() === 'html') { this.stopMonitoring(true); } })); // This one is for IE - this.hooks.add(dom.addDisposableListener(lastSameOriginAncestor.window.document.body, 'mouseleave', (browserEvent: MouseEvent) => { + this._hooks.add(dom.addDisposableListener(lastSameOriginAncestor.window.document.body, 'mouseleave', (browserEvent: MouseEvent) => { this.stopMonitoring(true); })); } diff --git a/src/vs/base/browser/mouseEvent.ts b/src/vs/base/browser/mouseEvent.ts index 31e2099671..a20722bddb 100644 --- a/src/vs/base/browser/mouseEvent.ts +++ b/src/vs/base/browser/mouseEvent.ts @@ -12,6 +12,7 @@ export interface IMouseEvent { readonly leftButton: boolean; readonly middleButton: boolean; readonly rightButton: boolean; + readonly buttons: number; readonly target: HTMLElement; readonly detail: number; readonly posx: number; @@ -33,6 +34,7 @@ export class StandardMouseEvent implements IMouseEvent { public readonly leftButton: boolean; public readonly middleButton: boolean; public readonly rightButton: boolean; + public readonly buttons: number; public readonly target: HTMLElement; public detail: number; public readonly posx: number; @@ -49,6 +51,7 @@ export class StandardMouseEvent implements IMouseEvent { this.leftButton = e.button === 0; this.middleButton = e.button === 1; this.rightButton = e.button === 2; + this.buttons = e.buttons; this.target = e.target; diff --git a/src/vs/base/browser/ui/button/button.ts b/src/vs/base/browser/ui/button/button.ts index 8015afc2ca..a443546c8d 100644 --- a/src/vs/base/browser/ui/button/button.ts +++ b/src/vs/base/browser/ui/button/button.ts @@ -130,7 +130,7 @@ export class Button extends Disposable { applyStyles(): void { if (this._element) { const background = this.buttonBackground ? this.buttonBackground.toString() : ''; - const foreground = this.buttonForeground ? this.buttonForeground.toString() : null; + const foreground = this.buttonForeground ? this.buttonForeground.toString() : ''; const border = this.buttonBorder ? this.buttonBorder.toString() : ''; this._element.style.color = foreground; diff --git a/src/vs/base/browser/ui/checkbox/checkbox.ts b/src/vs/base/browser/ui/checkbox/checkbox.ts index af8b05d09d..711f291ce8 100644 --- a/src/vs/base/browser/ui/checkbox/checkbox.ts +++ b/src/vs/base/browser/ui/checkbox/checkbox.ts @@ -220,7 +220,7 @@ export class SimpleCheckbox extends Widget { } protected applyStyles(): void { - this.domNode.style.color = this.styles.checkboxForeground ? this.styles.checkboxForeground.toString() : null; + this.domNode.style.color = this.styles.checkboxForeground ? this.styles.checkboxForeground.toString() : ''; this.domNode.style.backgroundColor = this.styles.checkboxBackground ? this.styles.checkboxBackground.toString() : ''; this.domNode.style.borderColor = this.styles.checkboxBorder ? this.styles.checkboxBorder.toString() : ''; } diff --git a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css index a4e1f97d52..f9daecb3e9 100644 --- a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css +++ b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css @@ -5,7 +5,7 @@ @font-face { font-family: "codicon"; - src: url("./codicon.ttf?17db7f5e5f31fd546e62218bb0823c0c") format("truetype"); + src: url("./codicon.ttf?ed926e87ee4e27771159d875e877f74a") format("truetype"); } .codicon[class*='codicon-'] { @@ -409,4 +409,5 @@ .codicon-call-outgoing:before { content: "\eb93" } .codicon-menu:before { content: "\eb94" } .codicon-expand-all:before { content: "\eb95" } +.codicon-feedback:before { content: "\eb96" } .codicon-debug-alt:before { content: "\f101" } diff --git a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf index 8788b6e97d..aae19797fb 100644 Binary files a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf and b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf differ diff --git a/src/vs/base/browser/ui/grid/gridview.ts b/src/vs/base/browser/ui/grid/gridview.ts index b040214086..83bce75206 100644 --- a/src/vs/base/browser/ui/grid/gridview.ts +++ b/src/vs/base/browser/ui/grid/gridview.ts @@ -392,6 +392,8 @@ class BranchNode implements ISplitView, IDisposable { const child = this._removeChild(from); this._addChild(child, to); + + this.onDidChildrenChange(); } swapChildren(from: number, to: number): void { @@ -408,6 +410,8 @@ class BranchNode implements ISplitView, IDisposable { this.splitview.swapViews(from, to); [this.children[from].orthogonalStartSash, this.children[from].orthogonalEndSash, this.children[to].orthogonalStartSash, this.children[to].orthogonalEndSash] = [this.children[to].orthogonalStartSash, this.children[to].orthogonalEndSash, this.children[from].orthogonalStartSash, this.children[from].orthogonalEndSash]; [this.children[from], this.children[to]] = [this.children[to], this.children[from]]; + + this.onDidChildrenChange(); } resizeChild(index: number, size: number): void { diff --git a/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts b/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts index 7d037e250a..1d582c6959 100644 --- a/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts +++ b/src/vs/base/browser/ui/highlightedlabel/highlightedLabel.ts @@ -10,6 +10,7 @@ import { escape } from 'vs/base/common/strings'; export interface IHighlight { start: number; end: number; + extraClasses?: string; } export class HighlightedLabel { @@ -69,7 +70,11 @@ export class HighlightedLabel { htmlContent += ''; pos = highlight.end; } - htmlContent += ''; + if (highlight.extraClasses) { + htmlContent += ``; + } else { + htmlContent += ``; + } const substring = this.text.substring(highlight.start, highlight.end); htmlContent += this.supportCodicons ? renderCodicons(escape(substring)) : escape(substring); htmlContent += ''; diff --git a/src/vs/base/browser/ui/inputbox/inputBox.ts b/src/vs/base/browser/ui/inputbox/inputBox.ts index d8e0e46aca..535f215cf8 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.ts +++ b/src/vs/base/browser/ui/inputbox/inputBox.ts @@ -511,7 +511,7 @@ export class InputBox extends Widget { const styles = this.stylesForType(this.message.type); spanElement.style.backgroundColor = styles.background ? styles.background.toString() : ''; - spanElement.style.color = styles.foreground ? styles.foreground.toString() : null; + spanElement.style.color = styles.foreground ? styles.foreground.toString() : ''; spanElement.style.border = styles.border ? `1px solid ${styles.border}` : ''; dom.append(div, spanElement); diff --git a/src/vs/base/browser/ui/menu/menu.ts b/src/vs/base/browser/ui/menu/menu.ts index 136045c92f..9d2cdac5e2 100644 --- a/src/vs/base/browser/ui/menu/menu.ts +++ b/src/vs/base/browser/ui/menu/menu.ts @@ -597,12 +597,12 @@ class BaseMenuActionViewItem extends BaseActionViewItem { const border = isSelected && this.menuStyle.selectionBorderColor ? `thin solid ${this.menuStyle.selectionBorderColor}` : ''; if (this.item) { - this.item.style.color = fgColor ? `${fgColor}` : null; - this.item.style.backgroundColor = bgColor ? `${bgColor}` : ''; + this.item.style.color = fgColor ? fgColor.toString() : ''; + this.item.style.backgroundColor = bgColor ? bgColor.toString() : ''; } if (this.check) { - this.check.style.color = fgColor ? `${fgColor}` : ''; + this.check.style.color = fgColor ? fgColor.toString() : ''; } if (this.container) { diff --git a/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts b/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts index 7d8b18d59c..9e6a493625 100644 --- a/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts +++ b/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts @@ -20,6 +20,7 @@ import { INewScrollPosition, Scrollable, ScrollbarVisibility } from 'vs/base/com const MOUSE_DRAG_RESET_DISTANCE = 140; export interface ISimplifiedMouseEvent { + buttons: number; posx: number; posy: number; } @@ -222,6 +223,7 @@ export abstract class AbstractScrollbar extends Widget { this.slider.toggleClassName('active', true); this._mouseMoveMonitor.startMonitoring( + e.buttons, standardMouseMoveMerger, (mouseMoveData: IStandardMouseMoveEventData) => { const mouseOrthogonalPosition = this._sliderOrthogonalMousePosition(mouseMoveData); diff --git a/src/vs/base/browser/ui/scrollbar/scrollbarArrow.ts b/src/vs/base/browser/ui/scrollbar/scrollbarArrow.ts index 4461e72442..7365ca8bb3 100644 --- a/src/vs/base/browser/ui/scrollbar/scrollbarArrow.ts +++ b/src/vs/base/browser/ui/scrollbar/scrollbarArrow.ts @@ -93,6 +93,7 @@ export class ScrollbarArrow extends Widget { this._mousedownScheduleRepeatTimer.cancelAndSet(scheduleRepeater, 200); this._mouseMoveMonitor.startMonitoring( + e.buttons, standardMouseMoveMerger, (mouseMoveData: IStandardMouseMoveEventData) => { /* Intentional empty */ diff --git a/src/vs/base/browser/ui/splitview/paneview.ts b/src/vs/base/browser/ui/splitview/paneview.ts index 54b84489f2..800329ad21 100644 --- a/src/vs/base/browser/ui/splitview/paneview.ts +++ b/src/vs/base/browser/ui/splitview/paneview.ts @@ -230,7 +230,7 @@ export abstract class Pane extends Disposable implements IView { toggleClass(this.header, 'expanded', expanded); this.header.setAttribute('aria-expanded', String(expanded)); - this.header.style.color = this.styles.headerForeground ? this.styles.headerForeground.toString() : null; + this.header.style.color = this.styles.headerForeground ? this.styles.headerForeground.toString() : ''; this.header.style.backgroundColor = this.styles.headerBackground ? this.styles.headerBackground.toString() : ''; this.header.style.borderTop = this.styles.headerBorder ? `1px solid ${this.styles.headerBorder}` : ''; this._dropBackground = this.styles.dropBackground; diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index aa15045062..c2fc137dab 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -190,7 +190,13 @@ function asListOptions(modelProvider: () => ITreeModel { + return options.ariaProvider!.isChecked!(node.element); + } : undefined, + getRole: options.ariaProvider && options.ariaProvider.getRole ? (node) => { + return options.ariaProvider!.getRole!(node.element); + } : undefined } }; } diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index 4388d38b31..8fbae3e6aa 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -258,7 +258,20 @@ function asObjectTreeOptions(options?: IAsyncDataTreeOpt e => (options.expandOnlyOnTwistieClick as ((e: T) => boolean))(e.element as T) ) ), - ariaProvider: undefined, + ariaProvider: options.ariaProvider && { + getPosInSet(el, index) { + return options.ariaProvider!.getPosInSet(el.element as T, index); + }, + getSetSize(el, index, listLength) { + return options.ariaProvider!.getSetSize(el.element as T, index, listLength); + }, + getRole: options.ariaProvider!.getRole ? (el) => { + return options.ariaProvider!.getRole!(el.element as T); + } : undefined, + isChecked: options.ariaProvider!.isChecked ? (e) => { + return options.ariaProvider?.isChecked!(e.element as T); + } : undefined + }, additionalScrollHeight: options.additionalScrollHeight }; } diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index 17e8bc8ecc..fe52899ca1 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -668,7 +668,7 @@ export class RunOnceWorker extends RunOnceScheduler { export interface IdleDeadline { readonly didTimeout: boolean; - timeRemaining(): DOMHighResTimeStamp; + timeRemaining(): number; } /** * Execute the callback the next time the browser is idle diff --git a/src/vs/base/common/buffer.ts b/src/vs/base/common/buffer.ts index 320613a3f6..1bc9a4d41e 100644 --- a/src/vs/base/common/buffer.ts +++ b/src/vs/base/common/buffer.ts @@ -6,7 +6,7 @@ import * as strings from 'vs/base/common/strings'; import * as streams from 'vs/base/common/stream'; -declare var Buffer: any; +declare const Buffer: any; const hasBuffer = (typeof Buffer !== 'undefined'); const hasTextEncoder = (typeof TextEncoder !== 'undefined'); diff --git a/src/vs/base/common/filters.ts b/src/vs/base/common/filters.ts index d247714d26..01ae3025d3 100644 --- a/src/vs/base/common/filters.ts +++ b/src/vs/base/common/filters.ts @@ -560,9 +560,9 @@ export function fuzzyScore(pattern: string, patternLow: string, patternStart: nu let wordPos = wordStart; // There will be a match, fill in tables - for (row = 1, patternPos = patternStart; patternPos < patternLen; row++ , patternPos++) { + for (row = 1, patternPos = patternStart; patternPos < patternLen; row++, patternPos++) { - for (column = 1, wordPos = wordStart; wordPos < wordLen; column++ , wordPos++) { + for (column = 1, wordPos = wordStart; wordPos < wordLen; column++, wordPos++) { const score = _doScore(pattern, patternLow, patternPos, patternStart, word, wordLow, wordPos); diff --git a/src/vs/base/common/lifecycle.ts b/src/vs/base/common/lifecycle.ts index ec33e251e0..ac1b7fa145 100644 --- a/src/vs/base/common/lifecycle.ts +++ b/src/vs/base/common/lifecycle.ts @@ -163,7 +163,7 @@ export abstract class Disposable implements IDisposable { /** * Manages the lifecycle of a disposable value that may be changed. * - * This ensures that when the the disposable value is changed, the previously held disposable is disposed of. You can + * This ensures that when the disposable value is changed, the previously held disposable is disposed of. You can * also register a `MutableDisposable` on a `Disposable` to ensure it is automatically cleaned up. */ export class MutableDisposable implements IDisposable { diff --git a/src/vs/base/common/map.ts b/src/vs/base/common/map.ts index 6290c3441b..ea33460391 100644 --- a/src/vs/base/common/map.ts +++ b/src/vs/base/common/map.ts @@ -454,8 +454,8 @@ export class ResourceMap { return this.map.delete(this.toKey(resource)); } - forEach(clb: (value: T) => void): void { - this.map.forEach(clb); + forEach(clb: (value: T, key: URI) => void): void { + this.map.forEach((value, index) => clb(value, URI.parse(index))); } values(): T[] { diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts index c4b3cdd9ec..af3a8d9333 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts @@ -12,47 +12,47 @@ export namespace Schemas { * A schema that is used for models that exist in memory * only and that have no correspondence on a server or such. */ - export const inMemory: string = 'inmemory'; + export const inMemory = 'inmemory'; /** * A schema that is used for setting files */ - export const vscode: string = 'vscode'; + export const vscode = 'vscode'; /** * A schema that is used for internal private files */ - export const internal: string = 'private'; + export const internal = 'private'; /** * A walk-through document. */ - export const walkThrough: string = 'walkThrough'; + export const walkThrough = 'walkThrough'; /** * An embedded code snippet. */ - export const walkThroughSnippet: string = 'walkThroughSnippet'; + export const walkThroughSnippet = 'walkThroughSnippet'; - export const http: string = 'http'; + export const http = 'http'; - export const https: string = 'https'; + export const https = 'https'; - export const file: string = 'file'; + export const file = 'file'; - export const mailto: string = 'mailto'; + export const mailto = 'mailto'; - export const untitled: string = 'untitled'; + export const untitled = 'untitled'; - export const data: string = 'data'; + export const data = 'data'; - export const command: string = 'command'; + export const command = 'command'; - export const vscodeRemote: string = 'vscode-remote'; + export const vscodeRemote = 'vscode-remote'; - export const vscodeRemoteResource: string = 'vscode-remote-resource'; + export const vscodeRemoteResource = 'vscode-remote-resource'; - export const userData: string = 'vscode-userdata'; + export const userData = 'vscode-userdata'; } class RemoteAuthoritiesImpl { diff --git a/src/vs/base/common/performance.js b/src/vs/base/common/performance.js index 06d070f529..a6079b5036 100644 --- a/src/vs/base/common/performance.js +++ b/src/vs/base/common/performance.js @@ -100,7 +100,7 @@ if (typeof global === 'object') { if (typeof define === 'function') { // amd define([], function () { return _factory(sharedObj); }); -} else if (typeof module === "object" && typeof module.exports === "object") { +} else if (typeof module === 'object' && typeof module.exports === 'object') { // commonjs module.exports = _factory(sharedObj); } else { diff --git a/src/vs/base/common/uint.ts b/src/vs/base/common/uint.ts index 1d901602ba..cf806e1c2e 100644 --- a/src/vs/base/common/uint.ts +++ b/src/vs/base/common/uint.ts @@ -57,12 +57,3 @@ export function toUint32(v: number): number { } return v | 0; } - -export function toUint32Array(arr: number[]): Uint32Array { - const len = arr.length; - const r = new Uint32Array(len); - for (let i = 0; i < len; i++) { - r[i] = toUint32(arr[i]); - } - return r; -} diff --git a/src/vs/base/common/uri.ts b/src/vs/base/common/uri.ts index dc6ad3e4ab..8512ff542e 100644 --- a/src/vs/base/common/uri.ts +++ b/src/vs/base/common/uri.ts @@ -390,7 +390,7 @@ interface UriState extends UriComponents { const _pathSepMarker = isWindows ? 1 : undefined; -// tslint:disable-next-line:class-name +// eslint-disable-next-line @typescript-eslint/class-name-casing class _URI extends URI { _formatted: string | null = null; diff --git a/src/vs/base/common/worker/simpleWorker.ts b/src/vs/base/common/worker/simpleWorker.ts index 1debaa2fbc..18b9c94b2a 100644 --- a/src/vs/base/common/worker/simpleWorker.ts +++ b/src/vs/base/common/worker/simpleWorker.ts @@ -12,7 +12,7 @@ const INITIALIZE = '$initialize'; export interface IWorker extends IDisposable { getId(): number; - postMessage(message: any, transfer: Transferable[]): void; + postMessage(message: any, transfer: ArrayBuffer[]): void; } export interface IWorkerCallback { @@ -302,7 +302,7 @@ export class SimpleWorkerServer { private _requestHandler: IRequestHandler | null; private _protocol: SimpleWorkerProtocol; - constructor(postMessage: (msg: any, transfer?: Transferable[]) => void, requestHandlerFactory: IRequestHandlerFactory | null) { + constructor(postMessage: (msg: any, transfer?: ArrayBuffer[]) => void, requestHandlerFactory: IRequestHandlerFactory | null) { this._requestHandlerFactory = requestHandlerFactory; this._requestHandler = null; this._protocol = new SimpleWorkerProtocol({ diff --git a/src/vs/base/node/encoding.ts b/src/vs/base/node/encoding.ts index 7b8bc76b5c..08fc6ca613 100644 --- a/src/vs/base/node/encoding.ts +++ b/src/vs/base/node/encoding.ts @@ -4,8 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as iconv from 'iconv-lite'; -import { isLinux, isMacintosh } from 'vs/base/common/platform'; -import { exec } from 'child_process'; import { Readable, Writable } from 'stream'; import { VSBuffer } from 'vs/base/common/buffer'; @@ -24,9 +22,10 @@ export const UTF16be_BOM = [0xFE, 0xFF]; export const UTF16le_BOM = [0xFF, 0xFE]; export const UTF8_BOM = [0xEF, 0xBB, 0xBF]; -const ZERO_BYTE_DETECTION_BUFFER_MAX_LEN = 512; // number of bytes to look at to decide about a file being binary or not -const NO_GUESS_BUFFER_MAX_LEN = 512; // when not auto guessing the encoding, small number of bytes are enough -const AUTO_GUESS_BUFFER_MAX_LEN = 512 * 8; // with auto guessing we want a lot more content to be read for guessing +const ZERO_BYTE_DETECTION_BUFFER_MAX_LEN = 512; // number of bytes to look at to decide about a file being binary or not +const NO_ENCODING_GUESS_MIN_BYTES = 512; // when not auto guessing the encoding, small number of bytes are enough +const AUTO_ENCODING_GUESS_MIN_BYTES = 512 * 8; // with auto guessing we want a lot more content to be read for guessing +const AUTO_ENCODING_GUESS_MAX_BYTES = 512 * 128; // set an upper limit for the number of bytes we pass on to jschardet export interface IDecodeStreamOptions { guessEncoding: boolean; @@ -42,7 +41,7 @@ export interface IDecodeStreamResult { export function toDecodeStream(readable: Readable, options: IDecodeStreamOptions): Promise { if (!options.minBytesRequiredForDetection) { - options.minBytesRequiredForDetection = options.guessEncoding ? AUTO_GUESS_BUFFER_MAX_LEN : NO_GUESS_BUFFER_MAX_LEN; + options.minBytesRequiredForDetection = options.guessEncoding ? AUTO_ENCODING_GUESS_MIN_BYTES : NO_ENCODING_GUESS_MIN_BYTES; } return new Promise((resolve, reject) => { @@ -212,7 +211,7 @@ const IGNORE_ENCODINGS = ['ascii', 'utf-16', 'utf-32']; async function guessEncodingByBuffer(buffer: Buffer): Promise { const jschardet = await import('jschardet'); - const guessed = jschardet.detect(buffer); + const guessed = jschardet.detect(buffer.slice(0, AUTO_ENCODING_GUESS_MAX_BYTES)); // ensure to limit buffer for guessing due to https://github.com/aadsm/jschardet/issues/53 if (!guessed || !guessed.encoding) { return null; } @@ -353,87 +352,3 @@ export function detectEncodingFromBuffer({ buffer, bytesRead }: IReadResult, aut return { seemsBinary, encoding }; } - -// https://ss64.com/nt/chcp.html -const windowsTerminalEncodings = { - '437': 'cp437', // United States - '850': 'cp850', // Multilingual(Latin I) - '852': 'cp852', // Slavic(Latin II) - '855': 'cp855', // Cyrillic(Russian) - '857': 'cp857', // Turkish - '860': 'cp860', // Portuguese - '861': 'cp861', // Icelandic - '863': 'cp863', // Canadian - French - '865': 'cp865', // Nordic - '866': 'cp866', // Russian - '869': 'cp869', // Modern Greek - '936': 'cp936', // Simplified Chinese - '1252': 'cp1252' // West European Latin -}; - -export async function resolveTerminalEncoding(verbose?: boolean): Promise { - let rawEncodingPromise: Promise; - - // Support a global environment variable to win over other mechanics - const cliEncodingEnv = process.env['VSCODE_CLI_ENCODING']; - if (cliEncodingEnv) { - if (verbose) { - console.log(`Found VSCODE_CLI_ENCODING variable: ${cliEncodingEnv}`); - } - - rawEncodingPromise = Promise.resolve(cliEncodingEnv); - } - - // Linux/Mac: use "locale charmap" command - else if (isLinux || isMacintosh) { - rawEncodingPromise = new Promise(resolve => { - if (verbose) { - console.log('Running "locale charmap" to detect terminal encoding...'); - } - - exec('locale charmap', (err, stdout, stderr) => resolve(stdout)); - }); - } - - // Windows: educated guess - else { - rawEncodingPromise = new Promise(resolve => { - if (verbose) { - console.log('Running "chcp" to detect terminal encoding...'); - } - - exec('chcp', (err, stdout, stderr) => { - if (stdout) { - const windowsTerminalEncodingKeys = Object.keys(windowsTerminalEncodings) as Array; - for (const key of windowsTerminalEncodingKeys) { - if (stdout.indexOf(key) >= 0) { - return resolve(windowsTerminalEncodings[key]); - } - } - } - - return resolve(undefined); - }); - }); - } - - const rawEncoding = await rawEncodingPromise; - if (verbose) { - console.log(`Detected raw terminal encoding: ${rawEncoding}`); - } - - if (!rawEncoding || rawEncoding.toLowerCase() === 'utf-8' || rawEncoding.toLowerCase() === UTF8) { - return UTF8; - } - - const iconvEncoding = toIconvLiteEncoding(rawEncoding); - if (iconv.encodingExists(iconvEncoding)) { - return iconvEncoding; - } - - if (verbose) { - console.log('Unsupported terminal encoding, falling back to UTF-8.'); - } - - return UTF8; -} diff --git a/src/vs/base/node/terminalEncoding.ts b/src/vs/base/node/terminalEncoding.ts new file mode 100644 index 0000000000..d50791e43c --- /dev/null +++ b/src/vs/base/node/terminalEncoding.ts @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * This code is also used by standalone cli's. Avoid adding dependencies to keep the size of the cli small. + */ +import { exec } from 'child_process'; +import * as os from 'os'; + +const windowsTerminalEncodings = { + '437': 'cp437', // United States + '850': 'cp850', // Multilingual(Latin I) + '852': 'cp852', // Slavic(Latin II) + '855': 'cp855', // Cyrillic(Russian) + '857': 'cp857', // Turkish + '860': 'cp860', // Portuguese + '861': 'cp861', // Icelandic + '863': 'cp863', // Canadian - French + '865': 'cp865', // Nordic + '866': 'cp866', // Russian + '869': 'cp869', // Modern Greek + '936': 'cp936', // Simplified Chinese + '1252': 'cp1252' // West European Latin +}; + +function toIconvLiteEncoding(encodingName: string): string { + const normalizedEncodingName = encodingName.replace(/[^a-zA-Z0-9]/g, '').toLowerCase(); + const mapped = JSCHARDET_TO_ICONV_ENCODINGS[normalizedEncodingName]; + + return mapped || normalizedEncodingName; +} + +const JSCHARDET_TO_ICONV_ENCODINGS: { [name: string]: string } = { + 'ibm866': 'cp866', + 'big5': 'cp950' +}; + +const UTF8 = 'utf8'; + + +export async function resolveTerminalEncoding(verbose?: boolean): Promise { + let rawEncodingPromise: Promise; + + // Support a global environment variable to win over other mechanics + const cliEncodingEnv = process.env['VSCODE_CLI_ENCODING']; + if (cliEncodingEnv) { + if (verbose) { + console.log(`Found VSCODE_CLI_ENCODING variable: ${cliEncodingEnv}`); + } + + rawEncodingPromise = Promise.resolve(cliEncodingEnv); + } + + // Windows: educated guess + else if (os.platform() === 'win32') { + rawEncodingPromise = new Promise(resolve => { + if (verbose) { + console.log('Running "chcp" to detect terminal encoding...'); + } + + exec('chcp', (err, stdout, stderr) => { + if (stdout) { + const windowsTerminalEncodingKeys = Object.keys(windowsTerminalEncodings) as Array; + for (const key of windowsTerminalEncodingKeys) { + if (stdout.indexOf(key) >= 0) { + return resolve(windowsTerminalEncodings[key]); + } + } + } + + return resolve(undefined); + }); + }); + } + // Linux/Mac: use "locale charmap" command + else { + rawEncodingPromise = new Promise(resolve => { + if (verbose) { + console.log('Running "locale charmap" to detect terminal encoding...'); + } + + exec('locale charmap', (err, stdout, stderr) => resolve(stdout)); + }); + } + + const rawEncoding = await rawEncodingPromise; + if (verbose) { + console.log(`Detected raw terminal encoding: ${rawEncoding}`); + } + + if (!rawEncoding || rawEncoding.toLowerCase() === 'utf-8' || rawEncoding.toLowerCase() === UTF8) { + return UTF8; + } + + return toIconvLiteEncoding(rawEncoding); +} diff --git a/src/vs/base/parts/ipc/common/ipc.ts b/src/vs/base/parts/ipc/common/ipc.ts index 51a8185c65..e93a9ffd81 100644 --- a/src/vs/base/parts/ipc/common/ipc.ts +++ b/src/vs/base/parts/ipc/common/ipc.ts @@ -187,7 +187,7 @@ const BufferPresets = { Object: createOneByteBuffer(DataType.Object), }; -declare var Buffer: any; +declare const Buffer: any; const hasBuffer = (typeof Buffer !== 'undefined'); function serialize(writer: IWriter, data: any): void { diff --git a/src/vs/base/parts/quickopen/browser/quickOpenWidget.ts b/src/vs/base/parts/quickopen/browser/quickOpenWidget.ts index 6c0906bf94..a44fc8008f 100644 --- a/src/vs/base/parts/quickopen/browser/quickOpenWidget.ts +++ b/src/vs/base/parts/quickopen/browser/quickOpenWidget.ts @@ -406,7 +406,7 @@ export class QuickOpenWidget extends Disposable implements IModelProvider, IThem protected applyStyles(): void { if (this.element) { - const foreground = this.styles.foreground ? this.styles.foreground.toString() : null; + const foreground = this.styles.foreground ? this.styles.foreground.toString() : ''; const background = this.styles.background ? this.styles.background.toString() : ''; const borderColor = this.styles.borderColor ? this.styles.borderColor.toString() : ''; const widgetShadow = this.styles.widgetShadow ? this.styles.widgetShadow.toString() : ''; diff --git a/src/vs/base/parts/quickopen/browser/quickopen.css b/src/vs/base/parts/quickopen/browser/quickopen.css index 0dbbd40c1f..6132b67329 100644 --- a/src/vs/base/parts/quickopen/browser/quickopen.css +++ b/src/vs/base/parts/quickopen/browser/quickopen.css @@ -77,7 +77,7 @@ } .monaco-quick-open-widget .quick-open-tree .monaco-icon-label, -.monaco-quick-open-widget .quick-open-tree .monaco-icon-label .monaco-icon-label-description-container { +.monaco-quick-open-widget .quick-open-tree .monaco-icon-label .monaco-icon-label-container > .monaco-icon-name-container { flex: 1; /* make sure the icon label grows within the row */ } diff --git a/src/vs/base/parts/quickopen/common/quickOpen.ts b/src/vs/base/parts/quickopen/common/quickOpen.ts index de59fcb2de..fb7de11df1 100644 --- a/src/vs/base/parts/quickopen/common/quickOpen.ts +++ b/src/vs/base/parts/quickopen/common/quickOpen.ts @@ -68,9 +68,7 @@ export interface IDataSource { export interface IRenderer { getHeight(entry: T): number; getTemplateId(entry: T): string; - // rationale: will be replaced by quickinput later - // tslint:disable-next-line: no-dom-globals - renderTemplate(templateId: string, container: HTMLElement, styles: any): any; + renderTemplate(templateId: string, container: any /* HTMLElement */, styles: any): any; renderElement(entry: T, templateId: string, templateData: any, styles: any): void; disposeTemplate(templateId: string, templateData: any): void; } diff --git a/src/vs/base/parts/storage/test/node/storage.test.ts b/src/vs/base/parts/storage/test/node/storage.test.ts index b244d1b415..20f156e865 100644 --- a/src/vs/base/parts/storage/test/node/storage.test.ts +++ b/src/vs/base/parts/storage/test/node/storage.test.ts @@ -137,14 +137,14 @@ suite('Storage Library', () => { changes.clear(); // Delete is accepted - change.set('foo', undefined); + change.set('foo', undefined!); database.fireDidChangeItemsExternal({ items: change }); ok(changes.has('foo')); equal(storage.get('foo', null!), null); changes.clear(); // Nothing happens if changing to same value - change.set('foo', undefined); + change.set('foo', undefined!); database.fireDidChangeItemsExternal({ items: change }); equal(changes.size, 0); diff --git a/src/vs/base/test/common/event.test.ts b/src/vs/base/test/common/event.test.ts index 71f299f824..b480564a15 100644 --- a/src/vs/base/test/common/event.test.ts +++ b/src/vs/base/test/common/event.test.ts @@ -136,6 +136,7 @@ suite('Event', function () { let a = new Emitter(); let hit = false; a.event(function () { + // eslint-disable-next-line no-throw-literal throw 9; }); a.event(function () { diff --git a/src/vs/base/test/common/lazy.test.ts b/src/vs/base/test/common/lazy.test.ts index 155b4f2dd9..9cdf6b231f 100644 --- a/src/vs/base/test/common/lazy.test.ts +++ b/src/vs/base/test/common/lazy.test.ts @@ -47,7 +47,7 @@ suite('Lazy', () => { assert.deepEqual(innerLazy.getValue(), [1, 11]); }); - test('map should should handle error values', () => { + test('map should handle error values', () => { let outer = 0; let inner = 10; const outerLazy = new Lazy(() => { throw new Error(`${++outer}`); }); diff --git a/src/vs/base/test/node/encoding/encoding.test.ts b/src/vs/base/test/node/encoding/encoding.test.ts index 9bceda47ec..fc8a713aa6 100644 --- a/src/vs/base/test/node/encoding/encoding.test.ts +++ b/src/vs/base/test/node/encoding/encoding.test.ts @@ -6,6 +6,7 @@ import * as assert from 'assert'; import * as fs from 'fs'; import * as encoding from 'vs/base/node/encoding'; +import * as terminalEncoding from 'vs/base/node/terminalEncoding'; import { Readable } from 'stream'; import { getPathFromAmdModule } from 'vs/base/common/amd'; @@ -118,14 +119,14 @@ suite('Encoding', () => { }); test('resolve terminal encoding (detect)', async function () { - const enc = await encoding.resolveTerminalEncoding(); - assert.ok(encoding.encodingExists(enc)); + const enc = await terminalEncoding.resolveTerminalEncoding(); + assert.ok(enc.length > 0); }); test('resolve terminal encoding (environment)', async function () { process.env['VSCODE_CLI_ENCODING'] = 'utf16le'; - const enc = await encoding.resolveTerminalEncoding(); + const enc = await terminalEncoding.resolveTerminalEncoding(); assert.ok(encoding.encodingExists(enc)); assert.equal(enc, 'utf16le'); }); diff --git a/src/vs/base/test/node/keytar.test.ts b/src/vs/base/test/node/keytar.test.ts index 329ec334e9..f0c0c0804c 100644 --- a/src/vs/base/test/node/keytar.test.ts +++ b/src/vs/base/test/node/keytar.test.ts @@ -28,7 +28,7 @@ suite('Keytar', () => { try { await keytar.deletePassword(name, 'foo'); } finally { - // tslint:disable-next-line: no-unsafe-finally + // eslint-disable-next-line no-unsafe-finally throw err; } } diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 06ea58deb2..872b639574 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -50,19 +50,18 @@ import { IFileService } from 'vs/platform/files/common/files'; import { DiskFileSystemProvider } from 'vs/platform/files/electron-browser/diskFileSystemProvider'; import { Schemas } from 'vs/base/common/network'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService, ISettingsSyncService, IUserDataAuthTokenService } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; -import { UserDataSyncChannel, UserDataSyncUtilServiceClient } from 'vs/platform/userDataSync/common/userDataSyncIpc'; +import { UserDataSyncChannel, UserDataSyncUtilServiceClient, SettingsSyncChannel, UserDataAuthTokenServiceChannel, UserDataAutoSyncChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; import { IElectronService } from 'vs/platform/electron/node/electron'; import { LoggerService } from 'vs/platform/log/node/loggerService'; import { UserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSyncLog'; -import { IAuthTokenService } from 'vs/platform/auth/common/auth'; -import { AuthTokenService } from 'vs/platform/auth/electron-browser/authTokenService'; -import { AuthTokenChannel } from 'vs/platform/auth/common/authTokenIpc'; import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; import { KeytarCredentialsService } from 'vs/platform/credentials/node/credentialsService'; import { UserDataAutoSync } from 'vs/platform/userDataSync/electron-browser/userDataAutoSync'; +import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync'; +import { UserDataAuthTokenService } from 'vs/platform/userDataSync/common/userDataAuthTokenService'; export interface ISharedProcessConfiguration { readonly machineId: string; @@ -182,10 +181,11 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat services.set(IDiagnosticsService, new SyncDescriptor(DiagnosticsService)); services.set(ICredentialsService, new SyncDescriptor(KeytarCredentialsService)); - services.set(IAuthTokenService, new SyncDescriptor(AuthTokenService)); + services.set(IUserDataAuthTokenService, new SyncDescriptor(UserDataAuthTokenService)); services.set(IUserDataSyncLogService, new SyncDescriptor(UserDataSyncLogService)); services.set(IUserDataSyncUtilService, new UserDataSyncUtilServiceClient(server.getChannel('userDataSyncUtil', activeWindowRouter))); services.set(IUserDataSyncStoreService, new SyncDescriptor(UserDataSyncStoreService)); + services.set(ISettingsSyncService, new SyncDescriptor(SettingsSynchroniser)); services.set(IUserDataSyncService, new SyncDescriptor(UserDataSyncService)); registerConfiguration(); @@ -205,14 +205,22 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat const diagnosticsChannel = new DiagnosticsChannel(diagnosticsService); server.registerChannel('diagnostics', diagnosticsChannel); - const authTokenService = accessor.get(IAuthTokenService); - const authTokenChannel = new AuthTokenChannel(authTokenService); + const authTokenService = accessor.get(IUserDataAuthTokenService); + const authTokenChannel = new UserDataAuthTokenServiceChannel(authTokenService); server.registerChannel('authToken', authTokenChannel); + const settingsSyncService = accessor.get(ISettingsSyncService); + const settingsSyncChannel = new SettingsSyncChannel(settingsSyncService); + server.registerChannel('settingsSync', settingsSyncChannel); + const userDataSyncService = accessor.get(IUserDataSyncService); const userDataSyncChannel = new UserDataSyncChannel(userDataSyncService); server.registerChannel('userDataSync', userDataSyncChannel); + const userDataAutoSync = instantiationService2.createInstance(UserDataAutoSync); + const userDataAutoSyncChannel = new UserDataAutoSyncChannel(userDataAutoSync); + server.registerChannel('userDataAutoSync', userDataAutoSyncChannel); + // clean up deprecated extensions (extensionManagementService as ExtensionManagementService).removeDeprecatedExtensions(); // update localizations cache @@ -223,7 +231,7 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat instantiationService2.createInstance(LanguagePackCachedDataCleaner), instantiationService2.createInstance(StorageDataCleaner), instantiationService2.createInstance(LogsDataCleaner), - instantiationService2.createInstance(UserDataAutoSync) + userDataAutoSync )); disposables.add(extensionManagementService as ExtensionManagementService); }); diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index c715f4d517..443142a980 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -171,7 +171,7 @@ export class CodeApplication extends Disposable { app.on('web-contents-created', (_event: Event, contents) => { contents.on('will-attach-webview', (event: Event, webPreferences, params) => { - const isValidWebviewSource = (source: string): boolean => { + const isValidWebviewSource = (source: string | undefined): boolean => { if (!source) { return false; } @@ -191,11 +191,12 @@ export class CodeApplication extends Disposable { webPreferences.nodeIntegration = false; // Verify URLs being loaded - if (isValidWebviewSource(params.src) && isValidWebviewSource(webPreferences.preloadURL)) { + // https://github.com/electron/electron/issues/21553 + if (isValidWebviewSource(params.src) && isValidWebviewSource((webPreferences as { preloadURL: string }).preloadURL)) { return; } - delete webPreferences.preloadUrl; + delete (webPreferences as { preloadURL: string }).preloadURL; // https://github.com/electron/electron/issues/21553 // Otherwise prevent loading this.logService.error('webContents#web-contents-created: Prevented webview attach'); @@ -497,27 +498,27 @@ export class CodeApplication extends Disposable { this.logService.info(`Tracing: waiting for windows to get ready...`); let recordingStopped = false; - const stopRecording = (timeout: boolean) => { + const stopRecording = async (timeout: boolean) => { if (recordingStopped) { return; } recordingStopped = true; // only once - contentTracing.stopRecording(join(homedir(), `${product.applicationName}-${Math.random().toString(16).slice(-4)}.trace.txt`), path => { - if (!timeout) { - if (this.dialogMainService) { - this.dialogMainService.showMessageBox({ - type: 'info', - message: localize('trace.message', "Successfully created trace."), - detail: localize('trace.detail', "Please create an issue and manually attach the following file:\n{0}", path), - buttons: [localize('trace.ok', "Ok")] - }, withNullAsUndefined(BrowserWindow.getFocusedWindow())); - } - } else { - this.logService.info(`Tracing: data recorded (after 30s timeout) to ${path}`); + const path = await contentTracing.stopRecording(join(homedir(), `${product.applicationName}-${Math.random().toString(16).slice(-4)}.trace.txt`)); + + if (!timeout) { + if (this.dialogMainService) { + this.dialogMainService.showMessageBox({ + type: 'info', + message: localize('trace.message', "Successfully created trace."), + detail: localize('trace.detail', "Please create an issue and manually attach the following file:\n{0}", path), + buttons: [localize('trace.ok', "Ok")] + }, withNullAsUndefined(BrowserWindow.getFocusedWindow())); } - }); + } else { + this.logService.info(`Tracing: data recorded (after 30s timeout) to ${path}`); + } }; // Wait up to 30s before creating the trace anyways diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index b8929ca792..be27392a3f 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -8,7 +8,7 @@ import * as objects from 'vs/base/common/objects'; import * as nls from 'vs/nls'; import { Event as CommonEvent, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; -import { screen, BrowserWindow, systemPreferences, app, TouchBar, nativeImage, Rectangle, Display, TouchBarSegmentedControl, NativeImage, BrowserWindowConstructorOptions, SegmentedControlSegment } from 'electron'; +import { screen, BrowserWindow, systemPreferences, app, TouchBar, nativeImage, Rectangle, Display, TouchBarSegmentedControl, NativeImage, BrowserWindowConstructorOptions, SegmentedControlSegment, nativeTheme } from 'electron'; import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment'; import { ILogService } from 'vs/platform/log/common/log'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -347,9 +347,9 @@ export class CodeWindow extends Disposable implements ICodeWindow { }); this._win.webContents.session.webRequest.onHeadersReceived(null!, (details, callback) => { - const responseHeaders = details.responseHeaders as { [key: string]: string[] }; + const responseHeaders = details.responseHeaders as Record; - const contentType: string[] = (responseHeaders['content-type'] || responseHeaders['Content-Type']); + const contentType = (responseHeaders['content-type'] || responseHeaders['Content-Type']); if (contentType && Array.isArray(contentType) && contentType.some(x => x.toLowerCase().indexOf('image/svg') >= 0)) { return callback({ cancel: true }); } @@ -441,7 +441,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Inject headers when requests are incoming const urls = ['https://marketplace.visualstudio.com/*', 'https://*.vsassets.io/*']; this._win.webContents.session.webRequest.onBeforeSendHeaders({ urls }, (details, cb) => - this.marketplaceHeadersPromise.then(headers => cb({ cancel: false, requestHeaders: objects.assign(details.requestHeaders, headers) as { [key: string]: string | undefined } }))); + this.marketplaceHeadersPromise.then(headers => cb({ cancel: false, requestHeaders: objects.assign(details.requestHeaders, headers) as Record }))); } private onWindowError(error: WindowError): void { @@ -648,7 +648,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { if (windowConfig?.autoDetectHighContrast === false) { autoDetectHighContrast = false; } - windowConfiguration.highContrast = isWindows && autoDetectHighContrast && systemPreferences.isInvertedColorScheme(); + windowConfiguration.highContrast = isWindows && autoDetectHighContrast && nativeTheme.shouldUseInvertedColorScheme; windowConfiguration.accessibilitySupport = app.accessibilitySupportEnabled; // Title style related @@ -1007,22 +1007,22 @@ export class CodeWindow extends Disposable implements ICodeWindow { switch (visibility) { case ('default'): this._win.setMenuBarVisibility(!isFullscreen); - this._win.setAutoHideMenuBar(isFullscreen); + this._win.autoHideMenuBar = isFullscreen; break; case ('visible'): this._win.setMenuBarVisibility(true); - this._win.setAutoHideMenuBar(false); + this._win.autoHideMenuBar = false; break; case ('toggle'): this._win.setMenuBarVisibility(false); - this._win.setAutoHideMenuBar(true); + this._win.autoHideMenuBar = true; break; case ('hidden'): this._win.setMenuBarVisibility(false); - this._win.setAutoHideMenuBar(false); + this._win.autoHideMenuBar = false; break; } } diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index f523559052..2102f19fe4 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -14,10 +14,10 @@ import product from 'vs/platform/product/common/product'; import * as paths from 'vs/base/common/path'; import { whenDeleted, writeFileSync } from 'vs/base/node/pfs'; import { findFreePort, randomPort } from 'vs/base/node/ports'; -import { resolveTerminalEncoding } from 'vs/base/node/encoding'; import { isWindows, isLinux } from 'vs/base/common/platform'; import { ProfilingSession, Target } from 'v8-inspect-profiler'; import { isString } from 'vs/base/common/types'; +import { hasStdinWithoutTty, stdinDataListener, getStdinFilePath, readFromStdin } from 'vs/platform/environment/node/stdin'; function shouldSpawnCliProcess(argv: ParsedArgs): boolean { return !!argv['install-source'] @@ -142,91 +142,55 @@ export async function main(argv: string[]): Promise { }); } - let stdinWithoutTty: boolean = false; - try { - stdinWithoutTty = !process.stdin.isTTY; // Via https://twitter.com/MylesBorins/status/782009479382626304 - } catch (error) { - // Windows workaround for https://github.com/nodejs/node/issues/11656 - } - - const readFromStdin = args._.some(a => a === '-'); - if (readFromStdin) { + const hasReadStdinArg = args._.some(a => a === '-'); + if (hasReadStdinArg) { // remove the "-" argument when we read from stdin args._ = args._.filter(a => a !== '-'); argv = argv.filter(a => a !== '-'); } - let stdinFilePath: string; - if (stdinWithoutTty) { + let stdinFilePath: string | undefined; + if (hasStdinWithoutTty()) { // Read from stdin: we require a single "-" argument to be passed in order to start reading from // stdin. We do this because there is no reliable way to find out if data is piped to stdin. Just // checking for stdin being connected to a TTY is not enough (https://github.com/Microsoft/vscode/issues/40351) - if (args._.length === 0 && readFromStdin) { - // prepare temp file to read stdin to - stdinFilePath = paths.join(os.tmpdir(), `code-stdin-${Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 3)}.txt`); + if (args._.length === 0) { + if (hasReadStdinArg) { + stdinFilePath = getStdinFilePath(); - // open tmp file for writing - let stdinFileError: Error | undefined; - let stdinFileStream: fs.WriteStream; - try { - stdinFileStream = fs.createWriteStream(stdinFilePath); - } catch (error) { - stdinFileError = error; - } + // returns a file path where stdin input is written into (write in progress). + try { + readFromStdin(stdinFilePath, !!verbose); // throws error if file can not be written - if (!stdinFileError) { + // Make sure to open tmp file + addArg(argv, stdinFilePath); - // Pipe into tmp file using terminals encoding - resolveTerminalEncoding(verbose).then(async encoding => { - const iconv = await import('iconv-lite'); - const converterStream = iconv.decodeStream(encoding); - process.stdin.pipe(converterStream).pipe(stdinFileStream); - }); + // Enable --wait to get all data and ignore adding this to history + addArg(argv, '--wait'); + addArg(argv, '--skip-add-to-recently-opened'); + args.wait = true; - // Make sure to open tmp file - addArg(argv, stdinFilePath); - - // Enable --wait to get all data and ignore adding this to history - addArg(argv, '--wait'); - addArg(argv, '--skip-add-to-recently-opened'); - args.wait = true; - } - - if (verbose) { - if (stdinFileError) { - console.error(`Failed to create file to read via stdin: ${stdinFileError.toString()}`); - } else { console.log(`Reading from stdin via: ${stdinFilePath}`); + } catch (e) { + console.log(`Failed to create file to read via stdin: ${e.toString()}`); + stdinFilePath = undefined; } - } - } + } else { - // If the user pipes data via stdin but forgot to add the "-" argument, help by printing a message - // if we detect that data flows into via stdin after a certain timeout. - else if (args._.length === 0) { - processCallbacks.push(child => new Promise(c => { - const dataListener = () => { - if (isWindows) { - console.log(`Run with '${product.applicationName} -' to read output from another program (e.g. 'echo Hello World | ${product.applicationName} -').`); - } else { - console.log(`Run with '${product.applicationName} -' to read from stdin (e.g. 'ps aux | grep code | ${product.applicationName} -').`); + // If the user pipes data via stdin but forgot to add the "-" argument, help by printing a message + // if we detect that data flows into via stdin after a certain timeout. + processCallbacks.push(_ => stdinDataListener(1000).then(dataReceived => { + if (dataReceived) { + if (isWindows) { + console.log(`Run with '${product.applicationName} -' to read output from another program (e.g. 'echo Hello World | ${product.applicationName} -').`); + } else { + console.log(`Run with '${product.applicationName} -' to read from stdin (e.g. 'ps aux | grep code | ${product.applicationName} -').`); + } } - - c(undefined); - }; - - // wait for 1s maximum... - setTimeout(() => { - process.stdin.removeListener('data', dataListener); - - c(undefined); - }, 1000); - - // ...but finish early if we detect data - process.stdin.once('data', dataListener); - })); + })); + } } } diff --git a/src/vs/editor/browser/controller/mouseHandler.ts b/src/vs/editor/browser/controller/mouseHandler.ts index 522c951581..4beed992e3 100644 --- a/src/vs/editor/browser/controller/mouseHandler.ts +++ b/src/vs/editor/browser/controller/mouseHandler.ts @@ -10,7 +10,7 @@ import { RunOnceScheduler, TimeoutTimer } from 'vs/base/common/async'; import { Disposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; import { HitTestContext, IViewZoneData, MouseTarget, MouseTargetFactory, PointerHandlerLastRenderData } from 'vs/editor/browser/controller/mouseTarget'; -import * as editorBrowser from 'vs/editor/browser/editorBrowser'; +import { IMouseTarget, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { ClientCoordinates, EditorMouseEvent, EditorMouseEventFactory, GlobalEditorMouseMoveMonitor, createEditorPagePosition } from 'vs/editor/browser/editorDom'; import { ViewController } from 'vs/editor/browser/view/viewController'; import { EditorZoom } from 'vs/editor/common/config/editorZoom'; @@ -148,7 +148,7 @@ export class MouseHandler extends ViewEventHandler { } // --- end event handlers - public getTargetAtClientPoint(clientX: number, clientY: number): editorBrowser.IMouseTarget | null { + public getTargetAtClientPoint(clientX: number, clientY: number): IMouseTarget | null { const clientPos = new ClientCoordinates(clientX, clientY); const pos = clientPos.toPageCoordinates(); const editorPos = createEditorPagePosition(this.viewHelper.viewDomNode); @@ -160,7 +160,7 @@ export class MouseHandler extends ViewEventHandler { return this.mouseTargetFactory.createMouseTarget(this.viewHelper.getLastRenderData(), editorPos, pos, null); } - protected _createMouseTarget(e: EditorMouseEvent, testEventTarget: boolean): editorBrowser.IMouseTarget { + protected _createMouseTarget(e: EditorMouseEvent, testEventTarget: boolean): IMouseTarget { return this.mouseTargetFactory.createMouseTarget(this.viewHelper.getLastRenderData(), e.editorPos, e.pos, testEventTarget ? e.target : null); } @@ -210,12 +210,12 @@ export class MouseHandler extends ViewEventHandler { public _onMouseDown(e: EditorMouseEvent): void { const t = this._createMouseTarget(e, true); - const targetIsContent = (t.type === editorBrowser.MouseTargetType.CONTENT_TEXT || t.type === editorBrowser.MouseTargetType.CONTENT_EMPTY); - const targetIsGutter = (t.type === editorBrowser.MouseTargetType.GUTTER_GLYPH_MARGIN || t.type === editorBrowser.MouseTargetType.GUTTER_LINE_NUMBERS || t.type === editorBrowser.MouseTargetType.GUTTER_LINE_DECORATIONS); - const targetIsLineNumbers = (t.type === editorBrowser.MouseTargetType.GUTTER_LINE_NUMBERS); + const targetIsContent = (t.type === MouseTargetType.CONTENT_TEXT || t.type === MouseTargetType.CONTENT_EMPTY); + const targetIsGutter = (t.type === MouseTargetType.GUTTER_GLYPH_MARGIN || t.type === MouseTargetType.GUTTER_LINE_NUMBERS || t.type === MouseTargetType.GUTTER_LINE_DECORATIONS); + const targetIsLineNumbers = (t.type === MouseTargetType.GUTTER_LINE_NUMBERS); const selectOnLineNumbers = this._context.configuration.options.get(EditorOption.selectOnLineNumbers); - const targetIsViewZone = (t.type === editorBrowser.MouseTargetType.CONTENT_VIEW_ZONE || t.type === editorBrowser.MouseTargetType.GUTTER_VIEW_ZONE); - const targetIsWidget = (t.type === editorBrowser.MouseTargetType.CONTENT_WIDGET); + const targetIsViewZone = (t.type === MouseTargetType.CONTENT_VIEW_ZONE || t.type === MouseTargetType.GUTTER_VIEW_ZONE); + const targetIsWidget = (t.type === MouseTargetType.CONTENT_WIDGET); let shouldHandle = e.leftButton || e.middleButton; if (platform.isMacintosh && e.leftButton && e.ctrlKey) { @@ -269,7 +269,7 @@ class MouseDownOperation extends Disposable { private readonly _context: ViewContext; private readonly _viewController: ViewController; private readonly _viewHelper: IPointerHandlerHelper; - private readonly _createMouseTarget: (e: EditorMouseEvent, testEventTarget: boolean) => editorBrowser.IMouseTarget; + private readonly _createMouseTarget: (e: EditorMouseEvent, testEventTarget: boolean) => IMouseTarget; private readonly _getMouseColumn: (e: EditorMouseEvent) => number; private readonly _mouseMoveMonitor: GlobalEditorMouseMoveMonitor; @@ -284,7 +284,7 @@ class MouseDownOperation extends Disposable { context: ViewContext, viewController: ViewController, viewHelper: IPointerHandlerHelper, - createMouseTarget: (e: EditorMouseEvent, testEventTarget: boolean) => editorBrowser.IMouseTarget, + createMouseTarget: (e: EditorMouseEvent, testEventTarget: boolean) => IMouseTarget, getMouseColumn: (e: EditorMouseEvent) => number ) { super(); @@ -331,10 +331,10 @@ class MouseDownOperation extends Disposable { } } - public start(targetType: editorBrowser.MouseTargetType, e: EditorMouseEvent): void { + public start(targetType: MouseTargetType, e: EditorMouseEvent): void { this._lastMouseEvent = e; - this._mouseState.setStartedOnLineNumbers(targetType === editorBrowser.MouseTargetType.GUTTER_LINE_NUMBERS); + this._mouseState.setStartedOnLineNumbers(targetType === MouseTargetType.GUTTER_LINE_NUMBERS); this._mouseState.setStartButtons(e); this._mouseState.setModifiers(e); const position = this._findMousePosition(e, true); @@ -356,13 +356,14 @@ class MouseDownOperation extends Disposable { && e.detail < 2 // only single click on a selection can work && !this._isActive // the mouse is not down yet && !this._currentSelection.isEmpty() // we don't drag single cursor - && (position.type === editorBrowser.MouseTargetType.CONTENT_TEXT) // single click on text + && (position.type === MouseTargetType.CONTENT_TEXT) // single click on text && position.position && this._currentSelection.containsPosition(position.position) // single click on a selection ) { this._mouseState.isDragAndDrop = true; this._isActive = true; this._mouseMoveMonitor.startMonitoring( + e.buttons, createMouseMoveEventMerger(null), (e) => this._onMouseDownThenMove(e), () => { @@ -386,6 +387,7 @@ class MouseDownOperation extends Disposable { if (!this._isActive) { this._isActive = true; this._mouseMoveMonitor.startMonitoring( + e.buttons, createMouseMoveEventMerger(null), (e) => this._onMouseDownThenMove(e), () => this._stop() @@ -436,12 +438,12 @@ class MouseDownOperation extends Disposable { if (viewZoneData) { const newPosition = this._helpPositionJumpOverViewZone(viewZoneData); if (newPosition) { - return new MouseTarget(null, editorBrowser.MouseTargetType.OUTSIDE_EDITOR, mouseColumn, newPosition); + return new MouseTarget(null, MouseTargetType.OUTSIDE_EDITOR, mouseColumn, newPosition); } } const aboveLineNumber = viewLayout.getLineNumberAtVerticalOffset(verticalOffset); - return new MouseTarget(null, editorBrowser.MouseTargetType.OUTSIDE_EDITOR, mouseColumn, new Position(aboveLineNumber, 1)); + return new MouseTarget(null, MouseTargetType.OUTSIDE_EDITOR, mouseColumn, new Position(aboveLineNumber, 1)); } if (e.posy > editorContent.y + editorContent.height) { @@ -450,22 +452,22 @@ class MouseDownOperation extends Disposable { if (viewZoneData) { const newPosition = this._helpPositionJumpOverViewZone(viewZoneData); if (newPosition) { - return new MouseTarget(null, editorBrowser.MouseTargetType.OUTSIDE_EDITOR, mouseColumn, newPosition); + return new MouseTarget(null, MouseTargetType.OUTSIDE_EDITOR, mouseColumn, newPosition); } } const belowLineNumber = viewLayout.getLineNumberAtVerticalOffset(verticalOffset); - return new MouseTarget(null, editorBrowser.MouseTargetType.OUTSIDE_EDITOR, mouseColumn, new Position(belowLineNumber, model.getLineMaxColumn(belowLineNumber))); + return new MouseTarget(null, MouseTargetType.OUTSIDE_EDITOR, mouseColumn, new Position(belowLineNumber, model.getLineMaxColumn(belowLineNumber))); } const possibleLineNumber = viewLayout.getLineNumberAtVerticalOffset(viewLayout.getCurrentScrollTop() + (e.posy - editorContent.y)); if (e.posx < editorContent.x) { - return new MouseTarget(null, editorBrowser.MouseTargetType.OUTSIDE_EDITOR, mouseColumn, new Position(possibleLineNumber, 1)); + return new MouseTarget(null, MouseTargetType.OUTSIDE_EDITOR, mouseColumn, new Position(possibleLineNumber, 1)); } if (e.posx > editorContent.x + editorContent.width) { - return new MouseTarget(null, editorBrowser.MouseTargetType.OUTSIDE_EDITOR, mouseColumn, new Position(possibleLineNumber, model.getLineMaxColumn(possibleLineNumber))); + return new MouseTarget(null, MouseTargetType.OUTSIDE_EDITOR, mouseColumn, new Position(possibleLineNumber, model.getLineMaxColumn(possibleLineNumber))); } return null; @@ -483,7 +485,7 @@ class MouseDownOperation extends Disposable { return null; } - if (t.type === editorBrowser.MouseTargetType.CONTENT_VIEW_ZONE || t.type === editorBrowser.MouseTargetType.GUTTER_VIEW_ZONE) { + if (t.type === MouseTargetType.CONTENT_VIEW_ZONE || t.type === MouseTargetType.GUTTER_VIEW_ZONE) { const newPosition = this._helpPositionJumpOverViewZone(t.detail); if (newPosition) { return new MouseTarget(t.element, t.type, t.mouseColumn, newPosition, null, t.detail); diff --git a/src/vs/editor/browser/controller/mouseTarget.ts b/src/vs/editor/browser/controller/mouseTarget.ts index f32976b2f8..37eed5b0e0 100644 --- a/src/vs/editor/browser/controller/mouseTarget.ts +++ b/src/vs/editor/browser/controller/mouseTarget.ts @@ -79,7 +79,7 @@ interface IETextRange { setEndPoint(how: string, SourceRange: IETextRange): void; } -declare var IETextRange: { +declare const IETextRange: { prototype: IETextRange; new(): IETextRange; }; diff --git a/src/vs/editor/browser/controller/textAreaHandler.ts b/src/vs/editor/browser/controller/textAreaHandler.ts index 6594d31749..7bcfd73eb5 100644 --- a/src/vs/editor/browser/controller/textAreaHandler.ts +++ b/src/vs/editor/browser/controller/textAreaHandler.ts @@ -29,6 +29,7 @@ import { RenderingContext, RestrictedRenderingContext, HorizontalPosition } from import { ViewContext } from 'vs/editor/common/view/viewContext'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; +import { IEditorAriaOptions } from 'vs/editor/browser/editorBrowser'; export interface ITextAreaHandlerHelper { visibleRangeForPositionRelativeToEditor(lineNumber: number, column: number): HorizontalPosition | null; @@ -102,7 +103,7 @@ export class TextAreaHandler extends ViewPart { this._accessibilityPageSize = options.get(EditorOption.accessibilityPageSize); this._contentLeft = layoutInfo.contentLeft; this._contentWidth = layoutInfo.contentWidth; - this._contentHeight = layoutInfo.contentHeight; + this._contentHeight = layoutInfo.height; this._fontInfo = options.get(EditorOption.fontInfo); this._lineHeight = options.get(EditorOption.lineHeight); this._emptySelectionClipboard = options.get(EditorOption.emptySelectionClipboard); @@ -353,7 +354,7 @@ export class TextAreaHandler extends ViewPart { this._accessibilityPageSize = options.get(EditorOption.accessibilityPageSize); this._contentLeft = layoutInfo.contentLeft; this._contentWidth = layoutInfo.contentWidth; - this._contentHeight = layoutInfo.contentHeight; + this._contentHeight = layoutInfo.height; this._fontInfo = options.get(EditorOption.fontInfo); this._lineHeight = options.get(EditorOption.lineHeight); this._emptySelectionClipboard = options.get(EditorOption.emptySelectionClipboard); @@ -424,6 +425,18 @@ export class TextAreaHandler extends ViewPart { return this._lastRenderPosition; } + public setAriaOptions(options: IEditorAriaOptions): void { + if (options.activeDescendant) { + this.textArea.setAttribute('aria-haspopup', 'true'); + this.textArea.setAttribute('aria-autocomplete', 'list'); + this.textArea.setAttribute('aria-activedescendant', options.activeDescendant); + } else { + this.textArea.setAttribute('aria-haspopup', 'false'); + this.textArea.setAttribute('aria-autocomplete', 'both'); + this.textArea.removeAttribute('aria-activedescendant'); + } + } + // --- end view API private _primaryCursorPosition: Position = new Position(1, 1); diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 066d00b09e..f08ee077cc 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -319,6 +319,14 @@ export interface IOverviewRuler { setLayout(position: OverviewRulerPosition): void; } +/** + * Editor aria options. + * @internal + */ +export interface IEditorAriaOptions { + activeDescendant: string | undefined; +} + /** * A rich code editor. */ @@ -482,6 +490,11 @@ export interface ICodeEditor extends editorCommon.IEditor { * @event */ onDidLayoutChange(listener: (e: EditorLayoutInfo) => void): IDisposable; + /** + * An event emitted when the content width or content height in the editor has changed. + * @event + */ + onDidContentSizeChange(listener: (e: editorCommon.IContentSizeChangedEvent) => void): IDisposable; /** * An event emitted when the scroll in the editor has changed. * @event @@ -532,12 +545,12 @@ export interface ICodeEditor extends editorCommon.IEditor { setModel(model: ITextModel | null): void; /** - * @internal + * Gets all the editor computed options. */ getOptions(): IComputedEditorOptions; /** - * @internal + * Gets a specific editor option. */ getOption(id: T): FindComputedEditorOptionValueById; @@ -558,6 +571,11 @@ export interface ICodeEditor extends editorCommon.IEditor { */ setValue(newValue: string): void; + /** + * Get the width of the editor's content. + * This is information that is "erased" when computing `scrollWidth = Math.max(contentWidth, width)` + */ + getContentWidth(): number; /** * Get the scrollWidth of the editor's viewport. */ @@ -567,6 +585,11 @@ export interface ICodeEditor extends editorCommon.IEditor { */ getScrollLeft(): number; + /** + * Get the height of the editor's content. + * This is information that is "erased" when computing `scrollHeight = Math.max(contentHeight, height)` + */ + getContentHeight(): number; /** * Get the scrollHeight of the editor's viewport. */ @@ -689,6 +712,12 @@ export interface ICodeEditor extends editorCommon.IEditor { */ setHiddenAreas(ranges: IRange[]): void; + /** + * Sets the editor aria options, primarily the active descendent. + * @internal + */ + setAriaOptions(options: IEditorAriaOptions): void; + /** * @internal */ diff --git a/src/vs/editor/browser/editorDom.ts b/src/vs/editor/browser/editorDom.ts index 97104f762d..5baaf1fedc 100644 --- a/src/vs/editor/browser/editorDom.ts +++ b/src/vs/editor/browser/editorDom.ts @@ -172,7 +172,7 @@ export class EditorPointerEventFactory { export class GlobalEditorMouseMoveMonitor extends Disposable { private readonly _editorViewDomNode: HTMLElement; - protected readonly _globalMouseMoveMonitor: GlobalMouseMoveMonitor; + private readonly _globalMouseMoveMonitor: GlobalMouseMoveMonitor; private _keydownListener: IDisposable | null; constructor(editorViewDomNode: HTMLElement) { @@ -182,7 +182,7 @@ export class GlobalEditorMouseMoveMonitor extends Disposable { this._keydownListener = null; } - public startMonitoring(merger: EditorMouseEventMerger, mouseMoveCallback: (e: EditorMouseEvent) => void, onStopCallback: () => void): void { + public startMonitoring(initialButtons: number, merger: EditorMouseEventMerger, mouseMoveCallback: (e: EditorMouseEvent) => void, onStopCallback: () => void): void { // Add a <> keydown event listener that will cancel the monitoring // if something other than a modifier key is pressed @@ -199,7 +199,7 @@ export class GlobalEditorMouseMoveMonitor extends Disposable { return merger(lastEvent, new EditorMouseEvent(currentEvent, this._editorViewDomNode)); }; - this._globalMouseMoveMonitor.startMonitoring(myMerger, mouseMoveCallback, () => { + this._globalMouseMoveMonitor.startMonitoring(initialButtons, myMerger, mouseMoveCallback, () => { this._keydownListener!.dispose(); onStopCallback(); }); diff --git a/src/vs/editor/browser/services/bulkEditService.ts b/src/vs/editor/browser/services/bulkEditService.ts index 246270a0c3..a0edab2a19 100644 --- a/src/vs/editor/browser/services/bulkEditService.ts +++ b/src/vs/editor/browser/services/bulkEditService.ts @@ -7,22 +7,28 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { WorkspaceEdit } from 'vs/editor/common/modes'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IProgress, IProgressStep } from 'vs/platform/progress/common/progress'; +import { IDisposable } from 'vs/base/common/lifecycle'; export const IBulkEditService = createDecorator('IWorkspaceEditService'); - export interface IBulkEditOptions { editor?: ICodeEditor; progress?: IProgress; + showPreview?: boolean; + label?: string; } export interface IBulkEditResult { ariaSummary: string; } +export type IBulkEditPreviewHandler = (edit: WorkspaceEdit, options?: IBulkEditOptions) => Promise; + export interface IBulkEditService { _serviceBrand: undefined; + setPreviewHandler(handler: IBulkEditPreviewHandler): IDisposable; + apply(edit: WorkspaceEdit, options?: IBulkEditOptions): Promise; } diff --git a/src/vs/editor/browser/view/domLineBreaksComputer.ts b/src/vs/editor/browser/view/domLineBreaksComputer.ts new file mode 100644 index 0000000000..d0d182a269 --- /dev/null +++ b/src/vs/editor/browser/view/domLineBreaksComputer.ts @@ -0,0 +1,287 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ILineBreaksComputerFactory, LineBreakData, ILineBreaksComputer } from 'vs/editor/common/viewModel/splitLinesCollection'; +import { WrappingIndent } from 'vs/editor/common/config/editorOptions'; +import { FontInfo } from 'vs/editor/common/config/fontInfo'; +import { createStringBuilder, IStringBuilder } from 'vs/editor/common/core/stringBuilder'; +import { CharCode } from 'vs/base/common/charCode'; +import * as strings from 'vs/base/common/strings'; +import { Configuration } from 'vs/editor/browser/config/configuration'; + +export class DOMLineBreaksComputerFactory implements ILineBreaksComputerFactory { + + public static create(): DOMLineBreaksComputerFactory { + return new DOMLineBreaksComputerFactory(); + } + + constructor() { + } + + public createLineBreaksComputer(fontInfo: FontInfo, tabSize: number, wrappingColumn: number, wrappingIndent: WrappingIndent): ILineBreaksComputer { + tabSize = tabSize | 0; //@perf + wrappingColumn = +wrappingColumn; //@perf + + let requests: string[] = []; + return { + addRequest: (lineText: string, previousLineBreakData: LineBreakData | null) => { + requests.push(lineText); + }, + finalize: () => { + return createLineBreaks(requests, fontInfo, tabSize, wrappingColumn, wrappingIndent); + } + }; + } +} + +function createLineBreaks(requests: string[], fontInfo: FontInfo, tabSize: number, firstLineBreakColumn: number, wrappingIndent: WrappingIndent): (LineBreakData | null)[] { + if (firstLineBreakColumn === -1) { + const result: null[] = []; + for (let i = 0, len = requests.length; i < len; i++) { + result[i] = null; + } + return result; + } + + const overallWidth = Math.round(firstLineBreakColumn * fontInfo.typicalHalfwidthCharacterWidth); + + // Cannot respect WrappingIndent.Indent and WrappingIndent.DeepIndent because that would require + // two dom layouts, in order to first set the width of the first line, and then set the width of the wrapped lines + if (wrappingIndent === WrappingIndent.Indent || wrappingIndent === WrappingIndent.DeepIndent) { + wrappingIndent = WrappingIndent.Same; + } + + const containerDomNode = document.createElement('div'); + Configuration.applyFontInfoSlow(containerDomNode, fontInfo); + + const sb = createStringBuilder(10000); + const firstNonWhitespaceIndices: number[] = []; + const wrappedTextIndentLengths: number[] = []; + const renderLineContents: string[] = []; + const allCharOffsets: number[][] = []; + const allVisibleColumns: number[][] = []; + for (let i = 0; i < requests.length; i++) { + const lineContent = requests[i]; + + let firstNonWhitespaceIndex = 0; + let wrappedTextIndentLength = 0; + let width = overallWidth; + + if (wrappingIndent !== WrappingIndent.None) { + firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineContent); + if (firstNonWhitespaceIndex === -1) { + // all whitespace line + firstNonWhitespaceIndex = 0; + + } else { + // Track existing indent + + for (let i = 0; i < firstNonWhitespaceIndex; i++) { + const charWidth = ( + lineContent.charCodeAt(i) === CharCode.Tab + ? (tabSize - (wrappedTextIndentLength % tabSize)) + : 1 + ); + wrappedTextIndentLength += charWidth; + } + + const indentWidth = Math.ceil(fontInfo.spaceWidth * wrappedTextIndentLength); + + // Force sticking to beginning of line if no character would fit except for the indentation + if (indentWidth + fontInfo.typicalFullwidthCharacterWidth > overallWidth) { + firstNonWhitespaceIndex = 0; + wrappedTextIndentLength = 0; + } else { + width = overallWidth - indentWidth; + } + } + } + + const renderLineContent = lineContent.substr(firstNonWhitespaceIndex); + const tmp = renderLine(renderLineContent, wrappedTextIndentLength, tabSize, width, sb); + firstNonWhitespaceIndices[i] = firstNonWhitespaceIndex; + wrappedTextIndentLengths[i] = wrappedTextIndentLength; + renderLineContents[i] = renderLineContent; + allCharOffsets[i] = tmp[0]; + allVisibleColumns[i] = tmp[1]; + } + containerDomNode.innerHTML = sb.build(); + + containerDomNode.style.position = 'absolute'; + containerDomNode.style.top = '10000'; + document.body.appendChild(containerDomNode); + + let range = document.createRange(); + const lineDomNodes = Array.prototype.slice.call(containerDomNode.children, 0); + + let result: (LineBreakData | null)[] = []; + for (let i = 0; i < requests.length; i++) { + const lineDomNode = lineDomNodes[i]; + const breakOffsets: number[] | null = readLineBreaks(range, lineDomNode, renderLineContents[i], allCharOffsets[i]); + if (breakOffsets === null) { + result[i] = null; + continue; + } + + const firstNonWhitespaceIndex = firstNonWhitespaceIndices[i]; + const wrappedTextIndentLength = wrappedTextIndentLengths[i]; + const visibleColumns = allVisibleColumns[i]; + + const breakOffsetsVisibleColumn: number[] = []; + for (let j = 0, len = breakOffsets.length; j < len; j++) { + breakOffsetsVisibleColumn[j] = visibleColumns[breakOffsets[j]]; + } + + if (firstNonWhitespaceIndex !== 0) { + // All break offsets are relative to the renderLineContent, make them absolute again + for (let j = 0, len = breakOffsets.length; j < len; j++) { + breakOffsets[j] += firstNonWhitespaceIndex; + } + } + + result[i] = new LineBreakData(breakOffsets, breakOffsetsVisibleColumn, wrappedTextIndentLength); + } + + document.body.removeChild(containerDomNode); + return result; +} + +function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: number, width: number, sb: IStringBuilder): [number[], number[]] { + sb.appendASCIIString('
'); + // if (containsRTL) { + // sb.appendASCIIString('" dir="ltr'); + // } + + const len = lineContent.length; + let visibleColumn = initialVisibleColumn; + let charOffset = 0; + let charOffsets: number[] = []; + let visibleColumns: number[] = []; + let nextCharCode = (0 < len ? lineContent.charCodeAt(0) : CharCode.Null); + + for (let charIndex = 0; charIndex < len; charIndex++) { + charOffsets[charIndex] = charOffset; + visibleColumns[charIndex] = visibleColumn; + const charCode = nextCharCode; + nextCharCode = (charIndex + 1 < len ? lineContent.charCodeAt(charIndex + 1) : CharCode.Null); + let producedCharacters = 1; + let charWidth = 1; + switch (charCode) { + case CharCode.Tab: + producedCharacters = (tabSize - (visibleColumn % tabSize)); + charWidth = producedCharacters; + for (let space = 1; space <= producedCharacters; space++) { + if (space < producedCharacters) { + sb.write1(0xA0); //   + } else { + sb.appendASCII(CharCode.Space); + } + } + break; + + case CharCode.Space: + if (nextCharCode === CharCode.Space) { + sb.write1(0xA0); //   + } else { + sb.appendASCII(CharCode.Space); + } + break; + + case CharCode.LessThan: + sb.appendASCIIString('<'); + break; + + case CharCode.GreaterThan: + sb.appendASCIIString('>'); + break; + + case CharCode.Ampersand: + sb.appendASCIIString('&'); + break; + + case CharCode.Null: + sb.appendASCIIString('�'); + break; + + case CharCode.UTF8_BOM: + case CharCode.LINE_SEPARATOR_2028: + sb.write1(0xFFFD); + break; + + default: + if (strings.isFullWidthCharacter(charCode)) { + charWidth++; + } + // if (renderControlCharacters && charCode < 32) { + // sb.write1(9216 + charCode); + // } else { + sb.write1(charCode); + // } + } + + charOffset += producedCharacters; + visibleColumn += charWidth; + } + + charOffsets[lineContent.length] = charOffset; + visibleColumns[lineContent.length] = visibleColumn; + + sb.appendASCIIString('
'); + + return [charOffsets, visibleColumns]; +} + +function readLineBreaks(range: Range, lineDomNode: HTMLDivElement, lineContent: string, charOffsets: number[]): number[] | null { + if (lineContent.length <= 1) { + return null; + } + const textContentNode = lineDomNode.firstChild!; + + const breakOffsets: number[] = []; + discoverBreaks(range, textContentNode, charOffsets, 0, null, lineContent.length - 1, null, breakOffsets); + + if (breakOffsets.length === 0) { + return null; + } + + breakOffsets.push(lineContent.length); + return breakOffsets; +} + +type MaybeRects = ClientRectList | DOMRectList | null; + +function discoverBreaks(range: Range, textContentNode: Node, charOffsets: number[], low: number, lowRects: MaybeRects, high: number, highRects: MaybeRects, result: number[]): void { + if (low === high) { + return; + } + + lowRects = lowRects || readClientRect(range, textContentNode, charOffsets[low], charOffsets[low + 1]); + highRects = highRects || readClientRect(range, textContentNode, charOffsets[high], charOffsets[high + 1]); + + if (Math.abs(lowRects[0].top - highRects[0].top) <= 0.1) { + // same line + return; + } + + // there is at least one line break between these two offsets + if (low + 1 === high) { + // the two characters are adjacent, so the line break must be exactly between them + result.push(high); + return; + } + + const mid = low + ((high - low) / 2) | 0; + const midRects = readClientRect(range, textContentNode, charOffsets[mid], charOffsets[mid + 1]); + discoverBreaks(range, textContentNode, charOffsets, low, lowRects, mid, midRects, result); + discoverBreaks(range, textContentNode, charOffsets, mid, midRects, high, highRects, result); +} + +function readClientRect(range: Range, textContentNode: Node, startOffset: number, endOffset: number): ClientRectList | DOMRectList { + range.setStart(textContentNode, startOffset); + range.setEnd(textContentNode, endOffset); + return range.getClientRects(); +} diff --git a/src/vs/editor/browser/view/viewImpl.ts b/src/vs/editor/browser/view/viewImpl.ts index eaa4e9cb7c..7f6f30e4a4 100644 --- a/src/vs/editor/browser/view/viewImpl.ts +++ b/src/vs/editor/browser/view/viewImpl.ts @@ -11,7 +11,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { IPointerHandlerHelper } from 'vs/editor/browser/controller/mouseHandler'; import { PointerHandler } from 'vs/editor/browser/controller/pointerHandler'; import { ITextAreaHandlerHelper, TextAreaHandler } from 'vs/editor/browser/controller/textAreaHandler'; -import { IContentWidget, IContentWidgetPosition, IOverlayWidget, IOverlayWidgetPosition, IMouseTarget, IViewZoneChangeAccessor } from 'vs/editor/browser/editorBrowser'; +import { IContentWidget, IContentWidgetPosition, IOverlayWidget, IOverlayWidgetPosition, IMouseTarget, IViewZoneChangeAccessor, IEditorAriaOptions } from 'vs/editor/browser/editorBrowser'; import { ICommandDelegate, ViewController } from 'vs/editor/browser/view/viewController'; import { ViewOutgoingEvents } from 'vs/editor/browser/view/viewOutgoingEvents'; import { ContentViewOverlays, MarginViewOverlays } from 'vs/editor/browser/view/viewOverlays'; @@ -308,6 +308,10 @@ export class View extends ViewEventHandler { this._applyLayout(); return false; } + public onContentSizeChanged(e: viewEvents.ViewContentSizeChangedEvent): boolean { + this.outgoingEvents.emitContentSizeChange(e); + return false; + } public onFocusChanged(e: viewEvents.ViewFocusChangedEvent): boolean { this.domNode.setClassName(this.getEditorClassName()); this._context.model.setHasFocus(e.isFocused); @@ -510,6 +514,10 @@ export class View extends ViewEventHandler { this._textAreaHandler.refreshFocusState(); } + public setAriaOptions(options: IEditorAriaOptions): void { + this._textAreaHandler.setAriaOptions(options); + } + public addContentWidget(widgetData: IContentWidgetData): void { this.contentWidgets.addWidget(widgetData.widget); this.layoutContentWidget(widgetData); @@ -559,4 +567,3 @@ function safeInvokeNoArg(func: Function): any { onUnexpectedError(e); } } - diff --git a/src/vs/editor/browser/view/viewOutgoingEvents.ts b/src/vs/editor/browser/view/viewOutgoingEvents.ts index 86b8049b02..f78eb51160 100644 --- a/src/vs/editor/browser/view/viewOutgoingEvents.ts +++ b/src/vs/editor/browser/view/viewOutgoingEvents.ts @@ -9,7 +9,7 @@ import { MouseTarget } from 'vs/editor/browser/controller/mouseTarget'; import { IEditorMouseEvent, IMouseTarget, IPartialEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { IScrollEvent } from 'vs/editor/common/editorCommon'; +import { IScrollEvent, IContentSizeChangedEvent } from 'vs/editor/common/editorCommon'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { IViewModel, ICoordinatesConverter } from 'vs/editor/common/viewModel/viewModel'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; @@ -20,6 +20,7 @@ export interface EventCallback { export class ViewOutgoingEvents extends Disposable { + public onDidContentSizeChange: EventCallback | null = null; public onDidScroll: EventCallback | null = null; public onDidGainFocus: EventCallback | null = null; public onDidLoseFocus: EventCallback | null = null; @@ -41,6 +42,12 @@ export class ViewOutgoingEvents extends Disposable { this._viewModel = viewModel; } + public emitContentSizeChange(e: viewEvents.ViewContentSizeChangedEvent): void { + if (this.onDidContentSizeChange) { + this.onDidContentSizeChange(e); + } + } + public emitScrollChanged(e: viewEvents.ViewScrollChangedEvent): void { if (this.onDidScroll) { this.onDidScroll(e); diff --git a/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts b/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts index 84f22c0729..9480b73578 100644 --- a/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts +++ b/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts @@ -56,7 +56,7 @@ export class EditorScrollbar extends ViewPart { fastScrollSensitivity: fastScrollSensitivity, }; - this.scrollbar = this._register(new SmoothScrollableElement(linesContent.domNode, scrollbarOptions, this._context.viewLayout.scrollable)); + this.scrollbar = this._register(new SmoothScrollableElement(linesContent.domNode, scrollbarOptions, this._context.viewLayout.getScrollable())); PartFingerprints.write(this.scrollbar.getDomNode(), PartFingerprint.ScrollableElement); this.scrollbarDomNode = createFastDomNode(this.scrollbar.getDomNode()); @@ -113,7 +113,7 @@ export class EditorScrollbar extends ViewPart { } else { this.scrollbarDomNode.setWidth(layoutInfo.contentWidth); } - this.scrollbarDomNode.setHeight(layoutInfo.contentHeight); + this.scrollbarDomNode.setHeight(layoutInfo.height); } public getOverviewRulerLayoutInfo(): IOverviewRulerLayoutInfo { diff --git a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts index 84f35e0f20..b2111aaaf1 100644 --- a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts +++ b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts @@ -136,14 +136,16 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { const indent = indents[lineIndex]; let result = ''; - const leftMostVisiblePosition = ctx.visibleRangeForPosition(new Position(lineNumber, 1)); - let left = leftMostVisiblePosition ? leftMostVisiblePosition.left : 0; - for (let i = 1; i <= indent; i++) { - const className = (containsActiveIndentGuide && i === activeIndentLevel ? 'cigra' : 'cigr'); - result += `
`; - left += indentWidth; - if (left > scrollWidth || (this._maxIndentLeft > 0 && left > this._maxIndentLeft)) { - break; + if (indent >= 1) { + const leftMostVisiblePosition = ctx.visibleRangeForPosition(new Position(lineNumber, 1)); + let left = leftMostVisiblePosition ? leftMostVisiblePosition.left : 0; + for (let i = 1; i <= indent; i++) { + const className = (containsActiveIndentGuide && i === activeIndentLevel ? 'cigra' : 'cigr'); + result += `
`; + left += indentWidth; + if (left > scrollWidth || (this._maxIndentLeft > 0 && left > this._maxIndentLeft)) { + break; + } } } diff --git a/src/vs/editor/browser/viewParts/lines/viewLine.ts b/src/vs/editor/browser/viewParts/lines/viewLine.ts index 5e0b2de104..009918f17c 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLine.ts @@ -213,6 +213,7 @@ export class ViewLine implements IVisibleLine { lineData.tokens, actualInlineDecorations, lineData.tabSize, + lineData.startVisibleColumn, options.spaceWidth, options.stopRenderingLineAfter, options.renderWhitespace, @@ -513,9 +514,16 @@ class RenderedViewLine implements IRenderedViewLine { return 0; } if (this._containsForeignElements === ForeignElementType.Before) { - // We have foreign element before the (empty) line + // We have foreign elements before the (empty) line return this.getWidth(); } + // We have foreign elements before & after the (empty) line + const readingTarget = this._getReadingTarget(domNode); + if (readingTarget.firstChild) { + return (readingTarget.firstChild).offsetWidth; + } else { + return 0; + } } if (this._pixelOffsetCache !== null) { diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.ts b/src/vs/editor/browser/viewParts/minimap/minimap.ts index d7f1c47b0c..d85f3e03ea 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimap.ts @@ -555,6 +555,7 @@ export class Minimap extends ViewPart { this._slider.toggleClassName('active', true); this._sliderMouseMoveMonitor.startMonitoring( + e.buttons, standardMouseMoveMerger, (mouseMoveData: IStandardMouseMoveEventData) => { const mouseOrthogonalDelta = Math.abs(mouseMoveData.posx - initialMouseOrthogonalPosition); diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index a6b6ed7241..d0630d55b2 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -50,6 +50,8 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { withNullAsUndefined } from 'vs/base/common/types'; +import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/monospaceLineBreaksComputer'; +import { DOMLineBreaksComputerFactory } from 'vs/editor/browser/view/domLineBreaksComputer'; let EDITOR_ID = 0; @@ -193,6 +195,9 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE private readonly _onKeyDown: Emitter = this._register(new Emitter()); public readonly onKeyDown: Event = this._onKeyDown.event; + private readonly _onDidContentSizeChange: Emitter = this._register(new Emitter()); + public readonly onDidContentSizeChange: Event = this._onDidContentSizeChange.event; + private readonly _onDidScrollChange: Emitter = this._register(new Emitter()); public readonly onDidScrollChange: Event = this._onDidScrollChange.event; @@ -757,6 +762,13 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE this._modelData.cursor.setSelections(source, ranges); } + public getContentWidth(): number { + if (!this._modelData) { + return -1; + } + return this._modelData.viewModel.viewLayout.getContentWidth(); + } + public getScrollWidth(): number { if (!this._modelData) { return -1; @@ -770,6 +782,13 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return this._modelData.viewModel.viewLayout.getCurrentScrollLeft(); } + public getContentHeight(): number { + if (!this._modelData) { + return -1; + } + return this._modelData.viewModel.viewLayout.getContentHeight(); + } + public getScrollHeight(): number { if (!this._modelData) { return -1; @@ -1311,6 +1330,13 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE this._modelData.view.render(true, forceRedraw); } + public setAriaOptions(options: editorBrowser.IEditorAriaOptions): void { + if (!this._modelData || !this._modelData.hasRealView) { + return; + } + this._modelData.view.setAriaOptions(options); + } + public applyFontInfo(target: HTMLElement): void { Configuration.applyFontInfoSlow(target, this._configuration.options.get(EditorOption.fontInfo)); } @@ -1329,7 +1355,14 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE model.onBeforeAttached(); - const viewModel = new ViewModel(this._id, this._configuration, model, (callback) => dom.scheduleAtNextAnimationFrame(callback)); + const viewModel = new ViewModel( + this._id, + this._configuration, + model, + DOMLineBreaksComputerFactory.create(), + MonospaceLineBreaksComputerFactory.create(this._configuration.options), + (callback) => dom.scheduleAtNextAnimationFrame(callback) + ); listenersToRemove.push(model.onDidChangeDecorations((e) => this._onDidChangeModelDecorations.fire(e))); listenersToRemove.push(model.onDidChangeLanguage((e) => { @@ -1463,6 +1496,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE } const viewOutgoingEvents = new ViewOutgoingEvents(viewModel); + viewOutgoingEvents.onDidContentSizeChange = (e) => this._onDidContentSizeChange.fire(e); viewOutgoingEvents.onDidScroll = (e) => this._onDidScrollChange.fire(e); viewOutgoingEvents.onDidGainFocus = () => this._editorTextFocus.setValue(true); viewOutgoingEvents.onDidLoseFocus = () => this._editorTextFocus.setValue(false); diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index ab3a9fe54c..d8943af24c 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -1130,11 +1130,11 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE let scrollTop = this.modifiedEditor.getScrollTop(); let scrollHeight = this.modifiedEditor.getScrollHeight(); - let computedAvailableSize = Math.max(0, layoutInfo.contentHeight); + let computedAvailableSize = Math.max(0, layoutInfo.height); let computedRepresentableSize = Math.max(0, computedAvailableSize - 2 * 0); let computedRatio = scrollHeight > 0 ? (computedRepresentableSize / scrollHeight) : 0; - let computedSliderSize = Math.max(0, Math.floor(layoutInfo.contentHeight * computedRatio)); + let computedSliderSize = Math.max(0, Math.floor(layoutInfo.height * computedRatio)); let computedSliderPosition = Math.floor(scrollTop * computedRatio); return { @@ -1629,7 +1629,7 @@ const DECORATIONS = { }), lineInsertWithSign: ModelDecorationOptions.register({ className: 'line-insert', - linesDecorationsClassName: 'insert-sign', + linesDecorationsClassName: 'insert-sign codicon codicon-add', marginClassName: 'line-insert', isWholeLine: true }), @@ -1641,7 +1641,7 @@ const DECORATIONS = { }), lineDeleteWithSign: ModelDecorationOptions.register({ className: 'line-delete', - linesDecorationsClassName: 'delete-sign', + linesDecorationsClassName: 'delete-sign codicon codicon-remove', marginClassName: 'line-delete', isWholeLine: true @@ -2100,7 +2100,7 @@ class InlineViewZonesComputer extends ViewZonesComputer { if (this.renderIndicators) { let index = lineNumber - lineChange.originalStartLineNumber; marginHTML = marginHTML.concat([ - `
` + `
` ]); } } @@ -2162,6 +2162,7 @@ class InlineViewZonesComputer extends ViewZonesComputer { lineTokens, actualDecorations, tabSize, + 0, fontInfo.spaceWidth, options.get(EditorOption.stopRenderingLineAfter), options.get(EditorOption.renderWhitespace), diff --git a/src/vs/editor/browser/widget/diffReview.ts b/src/vs/editor/browser/widget/diffReview.ts index 6d7a9c20fc..5856e375a3 100644 --- a/src/vs/editor/browser/widget/diffReview.ts +++ b/src/vs/editor/browser/widget/diffReview.ts @@ -780,6 +780,7 @@ export class DiffReview extends Disposable { lineTokens, [], tabSize, + 0, fontInfo.spaceWidth, options.get(EditorOption.stopRenderingLineAfter), options.get(EditorOption.renderWhitespace), diff --git a/src/vs/editor/browser/widget/inlineDiffMargin.ts b/src/vs/editor/browser/widget/inlineDiffMargin.ts index f7c1378b44..7d4d900bad 100644 --- a/src/vs/editor/browser/widget/inlineDiffMargin.ts +++ b/src/vs/editor/browser/widget/inlineDiffMargin.ts @@ -9,7 +9,7 @@ import { Action } from 'vs/base/common/actions'; import { Disposable } from 'vs/base/common/lifecycle'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import * as editorBrowser from 'vs/editor/browser/editorBrowser'; +import { IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { Range } from 'vs/editor/common/core/range'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; @@ -57,7 +57,7 @@ export class InlineDiffMargin extends Disposable { this._marginDomNode.style.zIndex = '10'; this._diffActions = document.createElement('div'); - this._diffActions.className = 'lightbulb-glyph'; + this._diffActions.className = 'codicon codicon-lightbulb lightbulb-glyph'; this._diffActions.style.position = 'absolute'; const lineHeight = editor.getOption(EditorOption.lineHeight); const lineFeed = editor.getModel()!.getEOL(); @@ -150,8 +150,8 @@ export class InlineDiffMargin extends Disposable { })); - this._register(editor.onMouseMove((e: editorBrowser.IEditorMouseEvent) => { - if (e.target.type === editorBrowser.MouseTargetType.CONTENT_VIEW_ZONE || e.target.type === editorBrowser.MouseTargetType.GUTTER_VIEW_ZONE) { + this._register(editor.onMouseMove((e: IEditorMouseEvent) => { + if (e.target.type === MouseTargetType.CONTENT_VIEW_ZONE || e.target.type === MouseTargetType.GUTTER_VIEW_ZONE) { const viewZoneId = e.target.detail.viewZoneId; if (viewZoneId === this._viewZoneId) { @@ -165,12 +165,12 @@ export class InlineDiffMargin extends Disposable { } })); - this._register(editor.onMouseDown((e: editorBrowser.IEditorMouseEvent) => { + this._register(editor.onMouseDown((e: IEditorMouseEvent) => { if (!e.event.rightButton) { return; } - if (e.target.type === editorBrowser.MouseTargetType.CONTENT_VIEW_ZONE || e.target.type === editorBrowser.MouseTargetType.GUTTER_VIEW_ZONE) { + if (e.target.type === MouseTargetType.CONTENT_VIEW_ZONE || e.target.type === MouseTargetType.GUTTER_VIEW_ZONE) { const viewZoneId = e.target.detail.viewZoneId; if (viewZoneId === this._viewZoneId) { diff --git a/src/vs/editor/browser/widget/media/addition-dark.svg b/src/vs/editor/browser/widget/media/addition-dark.svg deleted file mode 100644 index 4d9389336b..0000000000 --- a/src/vs/editor/browser/widget/media/addition-dark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/editor/browser/widget/media/addition-light.svg b/src/vs/editor/browser/widget/media/addition-light.svg deleted file mode 100644 index 01a9de7d5a..0000000000 --- a/src/vs/editor/browser/widget/media/addition-light.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/editor/browser/widget/media/close-dark.svg b/src/vs/editor/browser/widget/media/close-dark.svg deleted file mode 100644 index 6d16d212ae..0000000000 --- a/src/vs/editor/browser/widget/media/close-dark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/editor/browser/widget/media/close-light.svg b/src/vs/editor/browser/widget/media/close-light.svg deleted file mode 100644 index 742fcae4ae..0000000000 --- a/src/vs/editor/browser/widget/media/close-light.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/editor/browser/widget/media/deletion-dark.svg b/src/vs/editor/browser/widget/media/deletion-dark.svg deleted file mode 100644 index 4c5a9c1e3a..0000000000 --- a/src/vs/editor/browser/widget/media/deletion-dark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/editor/browser/widget/media/deletion-light.svg b/src/vs/editor/browser/widget/media/deletion-light.svg deleted file mode 100644 index d12a8ee313..0000000000 --- a/src/vs/editor/browser/widget/media/deletion-light.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/editor/browser/widget/media/diffEditor.css b/src/vs/editor/browser/widget/media/diffEditor.css index 55f5d9b785..7765a27a16 100644 --- a/src/vs/editor/browser/widget/media/diffEditor.css +++ b/src/vs/editor/browser/widget/media/diffEditor.css @@ -37,11 +37,10 @@ .monaco-diff-editor .insert-sign, .monaco-editor .delete-sign, .monaco-diff-editor .delete-sign { - background-size: 60%; - opacity: 0.7; - background-repeat: no-repeat; - background-position: 75% center; - background-size: 11px 11px; + font-size: 11px !important; + opacity: 0.7 !important; + display: flex !important; + align-items: center; } .monaco-editor.hc-black .insert-sign, .monaco-diff-editor.hc-black .insert-sign, @@ -49,27 +48,6 @@ .monaco-diff-editor.hc-black .delete-sign { opacity: 1; } -.monaco-editor .insert-sign, -.monaco-diff-editor .insert-sign { - background-image: url('addition-light.svg'); -} -.monaco-editor .delete-sign, -.monaco-diff-editor .delete-sign { - background-image: url('deletion-light.svg'); -} - -.monaco-editor.vs-dark .insert-sign, -.monaco-diff-editor.vs-dark .insert-sign, -.monaco-editor.hc-black .insert-sign, -.monaco-diff-editor.hc-black .insert-sign { - background-image: url('addition-dark.svg'); -} -.monaco-editor.vs-dark .delete-sign, -.monaco-diff-editor.vs-dark .delete-sign, -.monaco-editor.hc-black .delete-sign, -.monaco-diff-editor.hc-black .delete-sign { - background-image: url('deletion-dark.svg'); -} .monaco-editor .inline-deleted-margin-view-zone { text-align: right; @@ -94,15 +72,6 @@ display: inline-block; } -.monaco-editor .margin-view-zones .inline-deleted-margin-view-zone .lightbulb-glyph { - background: url('lightbulb-light.svg') center center no-repeat; -} - -.monaco-editor.vs-dark .margin-view-zones .inline-deleted-margin-view-zone .lightbulb-glyph, -.monaco-editor.hc-dark .margin-view-zones .inline-deleted-margin-view-zone .lightbulb-glyph { - background: url('lightbulb-dark.svg') center center no-repeat; -} - .monaco-editor .margin-view-zones .lightbulb-glyph:hover { cursor: pointer; } diff --git a/src/vs/editor/browser/widget/media/diffReview.css b/src/vs/editor/browser/widget/media/diffReview.css index 1c4a1a7b0f..f4ed5d7364 100644 --- a/src/vs/editor/browser/widget/media/diffReview.css +++ b/src/vs/editor/browser/widget/media/diffReview.css @@ -58,10 +58,3 @@ height: 16px; margin: 2px 0; } -.monaco-diff-editor .action-label.icon.close-diff-review { - background: url('close-light.svg') center center no-repeat; -} -.monaco-diff-editor.hc-black .action-label.icon.close-diff-review, -.monaco-diff-editor.vs-dark .action-label.icon.close-diff-review { - background: url('close-dark.svg') center center no-repeat; -} diff --git a/src/vs/editor/browser/widget/media/lightbulb-dark.svg b/src/vs/editor/browser/widget/media/lightbulb-dark.svg deleted file mode 100644 index 5fe8931a81..0000000000 --- a/src/vs/editor/browser/widget/media/lightbulb-dark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/editor/browser/widget/media/lightbulb-light.svg b/src/vs/editor/browser/widget/media/lightbulb-light.svg deleted file mode 100644 index 191c566fd2..0000000000 --- a/src/vs/editor/browser/widget/media/lightbulb-light.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/vs/editor/common/commands/replaceCommand.ts b/src/vs/editor/common/commands/replaceCommand.ts index 3215e3acd9..26d159892d 100644 --- a/src/vs/editor/common/commands/replaceCommand.ts +++ b/src/vs/editor/common/commands/replaceCommand.ts @@ -122,17 +122,19 @@ export class ReplaceCommandThatPreservesSelection implements ICommand { private readonly _range: Range; private readonly _text: string; private readonly _initialSelection: Selection; + private readonly _forceMoveMarkers: boolean; private _selectionId: string | null; - constructor(editRange: Range, text: string, initialSelection: Selection) { + constructor(editRange: Range, text: string, initialSelection: Selection, forceMoveMarkers: boolean = false) { this._range = editRange; this._text = text; this._initialSelection = initialSelection; + this._forceMoveMarkers = forceMoveMarkers; this._selectionId = null; } public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void { - builder.addEditOperation(this._range, this._text); + builder.addTrackedEditOperation(this._range, this._text, this._forceMoveMarkers); this._selectionId = builder.trackSelection(this._initialSelection); } diff --git a/src/vs/editor/common/commands/shiftCommand.ts b/src/vs/editor/common/commands/shiftCommand.ts index 413bc86f12..2063d37e13 100644 --- a/src/vs/editor/common/commands/shiftCommand.ts +++ b/src/vs/editor/common/commands/shiftCommand.ts @@ -113,7 +113,7 @@ export class ShiftCommand implements ICommand { if (this._opts.useTabStops) { // keep track of previous line's "miss-alignment" let previousLineExtraSpaces = 0, extraSpaces = 0; - for (let lineNumber = startLine; lineNumber <= endLine; lineNumber++ , previousLineExtraSpaces = extraSpaces) { + for (let lineNumber = startLine; lineNumber <= endLine; lineNumber++, previousLineExtraSpaces = extraSpaces) { extraSpaces = 0; let lineText = model.getLineContent(lineNumber); let indentationEndIndex = strings.firstNonWhitespaceIndex(lineText); diff --git a/src/vs/editor/common/config/commonEditorConfig.ts b/src/vs/editor/common/config/commonEditorConfig.ts index c0cafe6a7b..69f2fe58b4 100644 --- a/src/vs/editor/common/config/commonEditorConfig.ts +++ b/src/vs/editor/common/config/commonEditorConfig.ts @@ -11,7 +11,7 @@ import * as arrays from 'vs/base/common/arrays'; import { IEditorOptions, editorOptionsRegistry, ValidatedEditorOptions, IEnvironmentalOptions, IComputedEditorOptions, ConfigurationChangedEvent, EDITOR_MODEL_DEFAULTS, EditorOption, FindComputedEditorOptionValueById } from 'vs/editor/common/config/editorOptions'; import { EditorZoom } from 'vs/editor/common/config/editorZoom'; import { BareFontInfo, FontInfo } from 'vs/editor/common/config/fontInfo'; -import * as editorCommon from 'vs/editor/common/editorCommon'; +import { IConfiguration, IDimension } from 'vs/editor/common/editorCommon'; import { ConfigurationScope, Extensions, IConfigurationNode, IConfigurationRegistry, IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; import { AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; @@ -278,7 +278,7 @@ function deepCloneAndMigrateOptions(_options: IEditorOptions): IEditorOptions { return options; } -export abstract class CommonEditorConfiguration extends Disposable implements editorCommon.IConfiguration { +export abstract class CommonEditorConfiguration extends Disposable implements IConfiguration { private _onDidChange = this._register(new Emitter()); public readonly onDidChange: Event = this._onDidChange.event; @@ -308,7 +308,7 @@ export abstract class CommonEditorConfiguration extends Disposable implements ed this._register(TabFocus.onDidChangeTabFocus(_ => this._recomputeOptions())); } - public observeReferenceElement(dimension?: editorCommon.IDimension): void { + public observeReferenceElement(dimension?: IDimension): void { } public dispose(): void { diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index f79ab53304..108dcf0ace 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -33,7 +33,6 @@ export type EditorAutoClosingOvertypeStrategy = 'always' | 'auto' | 'never'; /** * Configuration options for auto indentation in the editor - * @internal */ export const enum EditorAutoIndentStrategy { None = 0, @@ -262,21 +261,21 @@ export interface IEditorOptions { * Defaults to 'same' in vscode and to 'none' in monaco-editor. */ wrappingIndent?: 'none' | 'same' | 'indent' | 'deepIndent'; + /** + * Controls the wrapping algorithm to use. + * Defaults to 'monospace'. + */ + wrappingAlgorithm?: 'monospace' | 'dom'; /** * Configure word wrapping characters. A break will be introduced before these characters. - * Defaults to '{([+'. + * Defaults to '([{‘“〈《「『【〔([{「£¥$£¥++'. */ wordWrapBreakBeforeCharacters?: string; /** * Configure word wrapping characters. A break will be introduced after these characters. - * Defaults to ' \t})]?|&,;'. + * Defaults to ' \t})]?|/&.,;¢°′″‰℃、。。、¢,.:;?!%・・ゝゞヽヾーァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ々〻ァィゥェォャュョッー”〉》」』】〕)]}」'. */ wordWrapBreakAfterCharacters?: string; - /** - * Configure word wrapping characters. A break will be introduced after these characters only if no `wordWrapBreakBeforeCharacters` or `wordWrapBreakAfterCharacters` were found. - * Defaults to '.'. - */ - wordWrapBreakObtrusiveCharacters?: string; /** * Performance guard: Stop rendering a line after x characters. * Defaults to 10000. @@ -465,7 +464,7 @@ export interface IEditorOptions { */ codeActionsOnSaveTimeout?: number; /** - * Enable code folding + * Enable code folding. * Defaults to true. */ folding?: boolean; @@ -474,6 +473,11 @@ export interface IEditorOptions { * Defaults to 'auto'. */ foldingStrategy?: 'auto' | 'indentation'; + /** + * Enable highlight for folded regions. + * Defaults to true. + */ + foldingHighlight?: boolean; /** * Controls whether the fold actions in the gutter stay always visible or hide unless the mouse is over the gutter. * Defaults to 'mouseover'. @@ -537,6 +541,11 @@ export interface IEditorOptions { * Controls fading out of unused variables. */ showUnused?: boolean; + /** + * Controls whether to focus the inline editor in the peek widget by default. + * Defaults to false. + */ + peekWidgetFocusInlineEditor?: boolean; } export interface IEditorConstructionOptions extends IEditorOptions { @@ -632,7 +641,7 @@ export class ValidatedEditorOptions { } /** - * @internal + * All computed editor options. */ export interface IComputedEditorOptions { get(id: T): FindComputedEditorOptionValueById; @@ -656,13 +665,10 @@ export interface IEnvironmentalOptions { readonly accessibilitySupport: AccessibilitySupport; } -/** - * @internal - */ export interface IEditorOption { readonly id: K1; readonly name: string; - readonly defaultValue: V; + defaultValue: V; /** * @internal */ @@ -996,7 +1002,6 @@ class EditorAccessibilitySupport extends BaseEditorOption>; class EditorFind extends BaseEditorOption { @@ -1360,9 +1361,6 @@ export interface IGotoLocationOptions { alternativeReferenceCommand?: string; } -/** - * @internal - */ export type GoToLocationOptions = Readonly>; class EditorGoToLocation extends BaseEditorOption { @@ -1492,9 +1490,6 @@ export interface IEditorHoverOptions { sticky?: boolean; } -/** - * @internal - */ export type EditorHoverOptions = Readonly>; class EditorHover extends BaseEditorOption { @@ -1542,6 +1537,55 @@ class EditorHover extends BaseEditorOption>; + +class EditorSemanticHighlighting extends BaseEditorOption { + + constructor() { + const defaults: EditorSemanticHighlightingOptions = { + enabled: true + }; + super( + EditorOption.semanticHighlighting, 'semanticHighlighting', defaults, + { + 'editor.semanticHighlighting.enabled': { + type: 'boolean', + default: defaults.enabled, + description: nls.localize('semanticHighlighting.enabled', "Controls whether the semanticHighlighting is shown for the languages that support it.") + } + } + ); + } + + public validate(_input: any): EditorSemanticHighlightingOptions { + if (typeof _input !== 'object') { + return this.defaultValue; + } + const input = _input as IEditorSemanticHighlightingOptions; + return { + enabled: EditorBooleanOption.boolean(input.enabled, this.defaultValue.enabled) + }; + } +} + +//#endregion + //#region layoutInfo /** @@ -1594,10 +1638,6 @@ export interface EditorLayoutInfo { * The width of the glyph margin. */ readonly glyphMarginWidth: number; - /** - * The height of the glyph margin. - */ - readonly glyphMarginHeight: number; /** * Left position for the line numbers. @@ -1607,10 +1647,6 @@ export interface EditorLayoutInfo { * The width of the line numbers. */ readonly lineNumbersWidth: number; - /** - * The height of the line numbers. - */ - readonly lineNumbersHeight: number; /** * Left position for the line decorations. @@ -1620,10 +1656,6 @@ export interface EditorLayoutInfo { * The width of the line decorations. */ readonly decorationsWidth: number; - /** - * The height of the line decorations. - */ - readonly decorationsHeight: number; /** * Left position for the content (actual text) @@ -1633,10 +1665,6 @@ export interface EditorLayoutInfo { * The width of the content (actual text) */ readonly contentWidth: number; - /** - * The height of the content (actual height) - */ - readonly contentHeight: number; /** * The position for the minimap @@ -1823,19 +1851,15 @@ export class EditorLayoutInfoComputer extends ComputedEditorOption>; class EditorLightbulb extends BaseEditorOption { @@ -1965,9 +1986,6 @@ export interface IEditorMinimapOptions { scale?: number; } -/** - * @internal - */ export type EditorMinimapOptions = Readonly>; class EditorMinimap extends BaseEditorOption { @@ -2069,9 +2087,6 @@ export interface IEditorParameterHintOptions { cycle?: boolean; } -/** - * @internal - */ export type InternalParameterHintOptions = Readonly>; class EditorParameterHints extends BaseEditorOption { @@ -2138,9 +2153,6 @@ export interface IQuickSuggestionsOptions { strings: boolean; } -/** - * @internal - */ export type ValidQuickSuggestionsOptions = boolean | Readonly>; class EditorQuickSuggestions extends BaseEditorOption { @@ -2217,9 +2229,6 @@ class EditorQuickSuggestions extends BaseEditorOption string); -/** - * @internal - */ export const enum RenderLineNumbersType { Off = 0, On = 1, @@ -2228,9 +2237,6 @@ export const enum RenderLineNumbersType { Custom = 4 } -/** - * @internal - */ export interface InternalEditorRenderLineNumbersOptions { readonly renderType: RenderLineNumbersType; readonly renderFn: ((lineNumber: number) => string) | null; @@ -2386,9 +2392,6 @@ export interface IEditorScrollbarOptions { horizontalSliderSize?: number; } -/** - * @internal - */ export interface InternalEditorScrollbarOptions { readonly arrowSize: number; readonly vertical: ScrollbarVisibility; @@ -2603,9 +2606,6 @@ export interface ISuggestOptions { showSnippets?: boolean; } -/** - * @internal - */ export type InternalSuggestOptions = Readonly>; class EditorSuggest extends BaseEditorOption { @@ -2829,7 +2829,7 @@ class EditorSuggest extends BaseEditorOption(option: IEditorOption): IEd return option; } -/** - * @internal - */ export const enum EditorOption { acceptSuggestionOnCommitCharacter, acceptSuggestionOnEnter, @@ -3091,6 +3084,7 @@ export const enum EditorOption { fixedOverflowWidgets, folding, foldingStrategy, + foldingHighlight, fontFamily, fontInfo, fontLigatures, @@ -3123,6 +3117,7 @@ export const enum EditorOption { overviewRulerBorder, overviewRulerLanes, parameterHints, + peekWidgetFocusInlineEditor, quickSuggestions, quickSuggestionsDelay, readOnly, @@ -3140,6 +3135,7 @@ export const enum EditorOption { selectionClipboard, selectionHighlight, selectOnLineNumbers, + semanticHighlighting, showFoldingControls, showUnused, snippetSuggestions, @@ -3156,10 +3152,10 @@ export const enum EditorOption { wordWrap, wordWrapBreakAfterCharacters, wordWrapBreakBeforeCharacters, - wordWrapBreakObtrusiveCharacters, wordWrapColumn, wordWrapMinified, wrappingIndent, + wrappingAlgorithm, // Leave these at the end (because they have dependencies!) editorClassName, @@ -3170,7 +3166,18 @@ export const enum EditorOption { } /** - * @internal + * WORKAROUND: TS emits "any" for complex editor options values (anything except string, bool, enum, etc. ends up being "any") + * @monacodtsreplace + * /accessibilitySupport, any/accessibilitySupport, AccessibilitySupport/ + * /find, any/find, EditorFindOptions/ + * /fontInfo, any/fontInfo, FontInfo/ + * /gotoLocation, any/gotoLocation, GoToLocationOptions/ + * /hover, any/hover, EditorHoverOptions/ + * /lightbulb, any/lightbulb, EditorLightbulbOptions/ + * /minimap, any/minimap, EditorMinimapOptions/ + * /parameterHints, any/parameterHints, InternalParameterHintOptions/ + * /quickSuggestions, any/quickSuggestions, ValidQuickSuggestionsOptions/ + * /suggest, any/suggest, InternalSuggestOptions/ */ export const EditorOptions = { acceptSuggestionOnCommitCharacter: register(new EditorBooleanOption( @@ -3358,6 +3365,10 @@ export const EditorOptions = { ['auto', 'indentation'] as const, { markdownDescription: nls.localize('foldingStrategy', "Controls the strategy for computing folding ranges. `auto` uses a language specific folding strategy, if available. `indentation` uses the indentation based folding strategy.") } )), + foldingHighlight: register(new EditorBooleanOption( + EditorOption.foldingHighlight, 'foldingHighlight', true, + { description: nls.localize('foldingHighlight', "Controls whether the editor should highlight folded ranges.") } + )), fontFamily: register(new EditorStringOption( EditorOption.fontFamily, 'fontFamily', EDITOR_FONT_DEFAULTS.fontFamily, { description: nls.localize('fontFamily', "Controls the font family.") } @@ -3483,6 +3494,10 @@ export const EditorOptions = { 3, 0, 3 )), parameterHints: register(new EditorParameterHints()), + peekWidgetFocusInlineEditor: register(new EditorBooleanOption( + EditorOption.peekWidgetFocusInlineEditor, 'peekWidgetFocusInlineEditor', false, + { description: nls.localize('peekWidgetFocusInlineEditor', "Controls whether to focus the inline editor in the peek widget by default.") } + )), quickSuggestions: register(new EditorQuickSuggestions()), quickSuggestionsDelay: register(new EditorIntOption( EditorOption.quickSuggestionsDelay, 'quickSuggestionsDelay', @@ -3565,6 +3580,7 @@ export const EditorOptions = { selectOnLineNumbers: register(new EditorBooleanOption( EditorOption.selectOnLineNumbers, 'selectOnLineNumbers', true, )), + semanticHighlighting: register(new EditorSemanticHighlighting()), showFoldingControls: register(new EditorStringEnumOption( EditorOption.showFoldingControls, 'showFoldingControls', 'mouseover' as 'always' | 'mouseover', @@ -3679,16 +3695,12 @@ export const EditorOptions = { )), wordWrapBreakAfterCharacters: register(new EditorStringOption( EditorOption.wordWrapBreakAfterCharacters, 'wordWrapBreakAfterCharacters', - ' \t})]?|/&,;¢°′″‰℃、。。、¢,.:;?!%・・ゝゞヽヾーァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ々〻ァィゥェォャュョッー”〉》」』】〕)]}」', + ' \t})]?|/&.,;¢°′″‰℃、。。、¢,.:;?!%・・ゝゞヽヾーァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ々〻ァィゥェォャュョッー”〉》」』】〕)]}」', )), wordWrapBreakBeforeCharacters: register(new EditorStringOption( EditorOption.wordWrapBreakBeforeCharacters, 'wordWrapBreakBeforeCharacters', '([{‘“〈《「『【〔([{「£¥$£¥++' )), - wordWrapBreakObtrusiveCharacters: register(new EditorStringOption( - EditorOption.wordWrapBreakObtrusiveCharacters, 'wordWrapBreakObtrusiveCharacters', - '.' - )), wordWrapColumn: register(new EditorIntOption( EditorOption.wordWrapColumn, 'wordWrapColumn', 80, 1, Constants.MAX_SAFE_SMALL_INTEGER, @@ -3720,28 +3732,28 @@ export const EditorOptions = { description: nls.localize('wrappingIndent', "Controls the indentation of wrapped lines."), } )), + wrappingAlgorithm: register(new EditorStringEnumOption( + EditorOption.wrappingAlgorithm, 'wrappingAlgorithm', + 'monospace' as 'monospace' | 'dom', + ['monospace', 'dom'] as const, + { + enumDescriptions: [ + nls.localize('wrappingAlgorithm.monospace', "Assumes that all characters are of the same width. This is a fast algorithm."), + nls.localize('wrappingAlgorithm.dom', "Delegates wrapping points computation to the DOM. This is a slow algorithm, that might cause freezes for large files.") + ], + description: nls.localize('wrappingAlgorithm', "Controls the algorithm that computes wrapping points.") + } + )), // Leave these at the end (because they have dependencies!) editorClassName: register(new EditorClassName()), pixelRatio: register(new EditorPixelRatio()), tabFocusMode: register(new EditorTabFocusMode()), layoutInfo: register(new EditorLayoutInfoComputer()), - wrappingInfo: register(new EditorWrappingInfoComputer()), + wrappingInfo: register(new EditorWrappingInfoComputer()) }; -/** - * @internal - */ type EditorOptionsType = typeof EditorOptions; -/** - * @internal - */ type FindEditorOptionsKeyById = { [K in keyof EditorOptionsType]: EditorOptionsType[K]['id'] extends T ? K : never }[keyof EditorOptionsType]; -/** - * @internal - */ type ComputedEditorOptionValue> = T extends IEditorOption ? R : never; -/** - * @internal - */ export type FindComputedEditorOptionValueById = NonNullable]>>; diff --git a/src/vs/editor/common/controller/cursor.ts b/src/vs/editor/common/controller/cursor.ts index ddb0215776..02275fbbd6 100644 --- a/src/vs/editor/common/controller/cursor.ts +++ b/src/vs/editor/common/controller/cursor.ts @@ -727,7 +727,7 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { case H.Paste: cursorChangeReason = CursorChangeReason.Paste; - this._paste(payload.text, payload.pasteOnNewLine, payload.multicursorText); + this._paste(payload.text, payload.pasteOnNewLine, payload.multicursorText || []); break; case H.Cut: @@ -1000,7 +1000,7 @@ class CommandExecutor { let operations: IIdentifiedSingleEditOperation[] = []; let operationMinor = 0; - const addEditOperation = (selection: Range, text: string | null) => { + const addEditOperation = (selection: Range, text: string | null, forceMoveMarkers: boolean = false) => { if (selection.isEmpty() && text === '') { // This command wants to add a no-op => no thank you return; @@ -1012,15 +1012,15 @@ class CommandExecutor { }, range: selection, text: text, - forceMoveMarkers: false, + forceMoveMarkers: forceMoveMarkers, isAutoWhitespaceEdit: command.insertsAutoWhitespace }); }; let hadTrackedEditOperation = false; - const addTrackedEditOperation = (selection: Range, text: string | null) => { + const addTrackedEditOperation = (selection: Range, text: string | null, forceMoveMarkers?: boolean) => { hadTrackedEditOperation = true; - addEditOperation(selection, text); + addEditOperation(selection, text, forceMoveMarkers); }; const trackSelection = (selection: Selection, trackPreviousOnEmpty?: boolean) => { diff --git a/src/vs/editor/common/controller/cursorTypeOperations.ts b/src/vs/editor/common/controller/cursorTypeOperations.ts index fe3dfd7ac0..17068f4e16 100644 --- a/src/vs/editor/common/controller/cursorTypeOperations.ts +++ b/src/vs/editor/common/controller/cursorTypeOperations.ts @@ -94,7 +94,7 @@ export class TypeOperations { if (pasteOnNewLine) { // Paste entire line at the beginning of line let typeSelection = new Range(position.lineNumber, 1, position.lineNumber, 1); - commands[i] = new ReplaceCommandThatPreservesSelection(typeSelection, text, selection); + commands[i] = new ReplaceCommandThatPreservesSelection(typeSelection, text, selection, true); } else { commands[i] = new ReplaceCommand(selection, text); } diff --git a/src/vs/editor/common/core/characterClassifier.ts b/src/vs/editor/common/core/characterClassifier.ts index c192a42f28..151bbacac9 100644 --- a/src/vs/editor/common/core/characterClassifier.ts +++ b/src/vs/editor/common/core/characterClassifier.ts @@ -12,14 +12,14 @@ export class CharacterClassifier { /** * Maintain a compact (fully initialized ASCII map for quickly classifying ASCII characters - used more often in code). */ - private _asciiMap: Uint8Array; + protected _asciiMap: Uint8Array; /** * The entire map (sparse array). */ - private _map: Map; + protected _map: Map; - private _defaultValue: number; + protected _defaultValue: number; constructor(_defaultValue: T) { let defaultValue = toUint8(_defaultValue); diff --git a/src/vs/editor/common/core/stringBuilder.ts b/src/vs/editor/common/core/stringBuilder.ts index c4d8d2ceda..3b53d1aa0e 100644 --- a/src/vs/editor/common/core/stringBuilder.ts +++ b/src/vs/editor/common/core/stringBuilder.ts @@ -5,7 +5,7 @@ import * as strings from 'vs/base/common/strings'; -declare var TextDecoder: any; // TODO@TypeScript +declare const TextDecoder: any; // TODO@TypeScript interface TextDecoder { decode(view: Uint16Array): string; } diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index 3c70f8432b..b60b0fb623 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -22,7 +22,7 @@ export interface IEditOperationBuilder { * @param range The range to replace (delete). May be empty to represent a simple insert. * @param text The text to replace with. May be null to represent a simple delete. */ - addEditOperation(range: Range, text: string | null): void; + addEditOperation(range: Range, text: string | null, forceMoveMarkers?: boolean): void; /** * Add a new edit operation (a replace operation). @@ -30,7 +30,7 @@ export interface IEditOperationBuilder { * @param range The range to replace (delete). May be empty to represent a simple insert. * @param text The text to replace with. May be null to represent a simple delete. */ - addTrackedEditOperation(range: Range, text: string | null): void; + addTrackedEditOperation(range: Range, text: string | null, forceMoveMarkers?: boolean): void; /** * Track `selection` when applying edit operations. @@ -174,6 +174,14 @@ export interface IScrollEvent { readonly scrollHeightChanged: boolean; } +export interface IContentSizeChangedEvent { + readonly contentWidth: number; + readonly contentHeight: number; + + readonly contentWidthChanged: boolean; + readonly contentHeightChanged: boolean; +} + export interface INewScrollPosition { scrollLeft?: number; scrollTop?: number; diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts index 764ca730fe..4c1fa536a2 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts @@ -511,7 +511,93 @@ export class PieceTreeBase { } public getLinesContent(): string[] { - return this.getContentOfSubTree(this.root).split(/\r\n|\r|\n/); + let lines: string[] = []; + let linesLength = 0; + let currentLine = ''; + let danglingCR = false; + + this.iterate(this.root, node => { + if (node === SENTINEL) { + return true; + } + + const piece = node.piece; + let pieceLength = piece.length; + if (pieceLength === 0) { + return true; + } + + const buffer = this._buffers[piece.bufferIndex].buffer; + const lineStarts = this._buffers[piece.bufferIndex].lineStarts; + + const pieceStartLine = piece.start.line; + const pieceEndLine = piece.end.line; + let pieceStartOffset = lineStarts[pieceStartLine] + piece.start.column; + + if (danglingCR) { + if (buffer.charCodeAt(pieceStartOffset) === CharCode.LineFeed) { + // pretend the \n was in the previous piece.. + pieceStartOffset++; + pieceLength--; + } + lines[linesLength++] = currentLine; + currentLine = ''; + danglingCR = false; + if (pieceLength === 0) { + return true; + } + } + + if (pieceStartLine === pieceEndLine) { + // this piece has no new lines + if (!this._EOLNormalized && buffer.charCodeAt(pieceStartOffset + pieceLength - 1) === CharCode.CarriageReturn) { + danglingCR = true; + currentLine += buffer.substr(pieceStartOffset, pieceLength - 1); + } else { + currentLine += buffer.substr(pieceStartOffset, pieceLength); + } + return true; + } + + // add the text before the first line start in this piece + currentLine += ( + this._EOLNormalized + ? buffer.substring(pieceStartOffset, Math.max(pieceStartOffset, lineStarts[pieceStartLine + 1] - this._EOLLength)) + : buffer.substring(pieceStartOffset, lineStarts[pieceStartLine + 1]).replace(/(\r\n|\r|\n)$/, '') + ); + lines[linesLength++] = currentLine; + + for (let line = pieceStartLine + 1; line < pieceEndLine; line++) { + currentLine = ( + this._EOLNormalized + ? buffer.substring(lineStarts[line], lineStarts[line + 1] - this._EOLLength) + : buffer.substring(lineStarts[line], lineStarts[line + 1]).replace(/(\r\n|\r|\n)$/, '') + ); + lines[linesLength++] = currentLine; + } + + if (!this._EOLNormalized && buffer.charCodeAt(lineStarts[pieceEndLine] + piece.end.column - 1) === CharCode.CarriageReturn) { + danglingCR = true; + if (piece.end.column === 0) { + // The last line ended with a \r, let's undo the push, it will be pushed by next iteration + linesLength--; + } else { + currentLine = buffer.substr(lineStarts[pieceEndLine], piece.end.column - 1); + } + } else { + currentLine = buffer.substr(lineStarts[pieceEndLine], piece.end.column); + } + + return true; + }); + + if (danglingCR) { + lines[linesLength++] = currentLine; + currentLine = ''; + } + + lines[linesLength++] = currentLine; + return lines; } public getLength(): number { @@ -728,7 +814,7 @@ export class PieceTreeBase { // #endregion // #region Piece Table - insert(offset: number, value: string, eolNormalized: boolean = false): void { + public insert(offset: number, value: string, eolNormalized: boolean = false): void { this._EOLNormalized = this._EOLNormalized && eolNormalized; this._lastVisitedLine.lineNumber = 0; this._lastVisitedLine.value = ''; @@ -826,7 +912,7 @@ export class PieceTreeBase { this.computeBufferMetadata(); } - delete(offset: number, cnt: number): void { + public delete(offset: number, cnt: number): void { this._lastVisitedLine.lineNumber = 0; this._lastVisitedLine.value = ''; @@ -899,7 +985,7 @@ export class PieceTreeBase { this.computeBufferMetadata(); } - insertContentToNodeLeft(value: string, node: TreeNode) { + private insertContentToNodeLeft(value: string, node: TreeNode) { // we are inserting content to the beginning of node let nodesToDel: TreeNode[] = []; if (this.shouldCheckCRLF() && this.endWithCR(value) && this.startWithLF(node)) { @@ -934,7 +1020,7 @@ export class PieceTreeBase { this.deleteNodes(nodesToDel); } - insertContentToNodeRight(value: string, node: TreeNode) { + private insertContentToNodeRight(value: string, node: TreeNode) { // we are inserting to the right of this node. if (this.adjustCarriageReturnFromNext(value, node)) { // move \n to the new node. @@ -952,9 +1038,9 @@ export class PieceTreeBase { this.validateCRLFWithPrevNode(newNode); } - positionInBuffer(node: TreeNode, remainder: number): BufferCursor; - positionInBuffer(node: TreeNode, remainder: number, ret: BufferCursor): null; - positionInBuffer(node: TreeNode, remainder: number, ret?: BufferCursor): BufferCursor | null { + private positionInBuffer(node: TreeNode, remainder: number): BufferCursor; + private positionInBuffer(node: TreeNode, remainder: number, ret: BufferCursor): null; + private positionInBuffer(node: TreeNode, remainder: number, ret?: BufferCursor): BufferCursor | null { let piece = node.piece; let bufferIndex = node.piece.bufferIndex; let lineStarts = this._buffers[bufferIndex].lineStarts; @@ -1002,7 +1088,7 @@ export class PieceTreeBase { }; } - getLineFeedCnt(bufferIndex: number, start: BufferCursor, end: BufferCursor): number { + private getLineFeedCnt(bufferIndex: number, start: BufferCursor, end: BufferCursor): number { // we don't need to worry about start: abc\r|\n, or abc|\r, or abc|\n, or abc|\r\n doesn't change the fact that, there is one line break after start. // now let's take care of end: abc\r|\n, if end is in between \r and \n, we need to add line feed count by 1 if (end.column === 0) { @@ -1032,18 +1118,18 @@ export class PieceTreeBase { } } - offsetInBuffer(bufferIndex: number, cursor: BufferCursor): number { + private offsetInBuffer(bufferIndex: number, cursor: BufferCursor): number { let lineStarts = this._buffers[bufferIndex].lineStarts; return lineStarts[cursor.line] + cursor.column; } - deleteNodes(nodes: TreeNode[]): void { + private deleteNodes(nodes: TreeNode[]): void { for (let i = 0; i < nodes.length; i++) { rbDelete(this, nodes[i]); } } - createNewPieces(text: string): Piece[] { + private createNewPieces(text: string): Piece[] { if (text.length > AverageBufferSize) { // the content is large, operations like substring, charCode becomes slow // so here we split it into smaller chunks, just like what we did for CR/LF normalization @@ -1128,11 +1214,11 @@ export class PieceTreeBase { return [newPiece]; } - getLinesRawContent(): string { + public getLinesRawContent(): string { return this.getContentOfSubTree(this.root); } - getLineRawContent(lineNumber: number, endOffset: number = 0): string { + public getLineRawContent(lineNumber: number, endOffset: number = 0): string { let x = this.root; let ret = ''; @@ -1204,7 +1290,7 @@ export class PieceTreeBase { return ret; } - computeBufferMetadata() { + private computeBufferMetadata() { let x = this.root; let lfCnt = 1; @@ -1222,7 +1308,7 @@ export class PieceTreeBase { } // #region node operations - getIndexOf(node: TreeNode, accumulatedValue: number): { index: number, remainder: number } { + private getIndexOf(node: TreeNode, accumulatedValue: number): { index: number, remainder: number } { let piece = node.piece; let pos = this.positionInBuffer(node, accumulatedValue); let lineCnt = pos.line - piece.start.line; @@ -1239,7 +1325,7 @@ export class PieceTreeBase { return { index: lineCnt, remainder: pos.column }; } - getAccumulatedValue(node: TreeNode, index: number) { + private getAccumulatedValue(node: TreeNode, index: number) { if (index < 0) { return 0; } @@ -1253,7 +1339,7 @@ export class PieceTreeBase { } } - deleteNodeTail(node: TreeNode, pos: BufferCursor) { + private deleteNodeTail(node: TreeNode, pos: BufferCursor) { const piece = node.piece; const originalLFCnt = piece.lineFeedCnt; const originalEndOffset = this.offsetInBuffer(piece.bufferIndex, piece.end); @@ -1277,7 +1363,7 @@ export class PieceTreeBase { updateTreeMetadata(this, node, size_delta, lf_delta); } - deleteNodeHead(node: TreeNode, pos: BufferCursor) { + private deleteNodeHead(node: TreeNode, pos: BufferCursor) { const piece = node.piece; const originalLFCnt = piece.lineFeedCnt; const originalStartOffset = this.offsetInBuffer(piece.bufferIndex, piece.start); @@ -1299,7 +1385,7 @@ export class PieceTreeBase { updateTreeMetadata(this, node, size_delta, lf_delta); } - shrinkNode(node: TreeNode, start: BufferCursor, end: BufferCursor) { + private shrinkNode(node: TreeNode, start: BufferCursor, end: BufferCursor) { const piece = node.piece; const originalStartPos = piece.start; const originalEndPos = piece.end; @@ -1334,7 +1420,7 @@ export class PieceTreeBase { this.validateCRLFWithPrevNode(newNode); } - appendToNode(node: TreeNode, value: string): void { + private appendToNode(node: TreeNode, value: string): void { if (this.adjustCarriageReturnFromNext(value, node)) { value += '\n'; } @@ -1374,7 +1460,7 @@ export class PieceTreeBase { updateTreeMetadata(this, node, value.length, lf_delta); } - nodeAt(offset: number): NodePosition { + private nodeAt(offset: number): NodePosition { let x = this.root; let cache = this._searchCache.get(offset); if (cache) { @@ -1409,7 +1495,7 @@ export class PieceTreeBase { return null!; } - nodeAt2(lineNumber: number, column: number): NodePosition { + private nodeAt2(lineNumber: number, column: number): NodePosition { let x = this.root; let nodeStartOffset = 0; @@ -1476,7 +1562,7 @@ export class PieceTreeBase { return null!; } - nodeCharCodeAt(node: TreeNode, offset: number): number { + private nodeCharCodeAt(node: TreeNode, offset: number): number { if (node.piece.lineFeedCnt < 1) { return -1; } @@ -1485,7 +1571,7 @@ export class PieceTreeBase { return buffer.buffer.charCodeAt(newOffset); } - offsetOfNode(node: TreeNode): number { + private offsetOfNode(node: TreeNode): number { if (!node) { return 0; } @@ -1504,11 +1590,11 @@ export class PieceTreeBase { // #endregion // #region CRLF - shouldCheckCRLF() { + private shouldCheckCRLF() { return !(this._EOLNormalized && this._EOL === '\n'); } - startWithLF(val: string | TreeNode): boolean { + private startWithLF(val: string | TreeNode): boolean { if (typeof val === 'string') { return val.charCodeAt(0) === 10; } @@ -1532,7 +1618,7 @@ export class PieceTreeBase { return this._buffers[piece.bufferIndex].buffer.charCodeAt(startOffset) === 10; } - endWithCR(val: string | TreeNode): boolean { + private endWithCR(val: string | TreeNode): boolean { if (typeof val === 'string') { return val.charCodeAt(val.length - 1) === 13; } @@ -1544,7 +1630,7 @@ export class PieceTreeBase { return this.nodeCharCodeAt(val, val.piece.length - 1) === 13; } - validateCRLFWithPrevNode(nextNode: TreeNode) { + private validateCRLFWithPrevNode(nextNode: TreeNode) { if (this.shouldCheckCRLF() && this.startWithLF(nextNode)) { let node = nextNode.prev(); if (this.endWithCR(node)) { @@ -1553,7 +1639,7 @@ export class PieceTreeBase { } } - validateCRLFWithNextNode(node: TreeNode) { + private validateCRLFWithNextNode(node: TreeNode) { if (this.shouldCheckCRLF() && this.endWithCR(node)) { let nextNode = node.next(); if (this.startWithLF(nextNode)) { @@ -1562,7 +1648,7 @@ export class PieceTreeBase { } } - fixCRLF(prev: TreeNode, next: TreeNode) { + private fixCRLF(prev: TreeNode, next: TreeNode) { let nodesToDel: TreeNode[] = []; // update node let lineStarts = this._buffers[prev.piece.bufferIndex].lineStarts; @@ -1617,7 +1703,7 @@ export class PieceTreeBase { } } - adjustCarriageReturnFromNext(value: string, node: TreeNode): boolean { + private adjustCarriageReturnFromNext(value: string, node: TreeNode): boolean { if (this.shouldCheckCRLF() && this.endWithCR(value)) { let nextNode = node.next(); if (this.startWithLF(nextNode)) { @@ -1667,7 +1753,7 @@ export class PieceTreeBase { return callback(node) && this.iterate(node.right, callback); } - getNodeContent(node: TreeNode) { + private getNodeContent(node: TreeNode) { if (node === SENTINEL) { return ''; } @@ -1695,7 +1781,7 @@ export class PieceTreeBase { * / * z */ - rbInsertRight(node: TreeNode | null, p: Piece): TreeNode { + private rbInsertRight(node: TreeNode | null, p: Piece): TreeNode { let z = new TreeNode(p, NodeColor.Red); z.left = SENTINEL; z.right = SENTINEL; @@ -1727,7 +1813,7 @@ export class PieceTreeBase { * \ * z */ - rbInsertLeft(node: TreeNode | null, p: Piece): TreeNode { + private rbInsertLeft(node: TreeNode | null, p: Piece): TreeNode { let z = new TreeNode(p, NodeColor.Red); z.left = SENTINEL; z.right = SENTINEL; @@ -1751,7 +1837,7 @@ export class PieceTreeBase { return z; } - getContentOfSubTree(node: TreeNode): string { + private getContentOfSubTree(node: TreeNode): string { let str = ''; this.iterate(node, node => { diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index b4b7eff15a..d33b96bae7 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -158,6 +158,17 @@ class TextModelSnapshot implements model.ITextSnapshot { const invalidFunc = () => { throw new Error(`Invalid change accessor`); }; +const enum StringOffsetValidationType { + /** + * Even allowed in surrogate pairs + */ + Relaxed = 0, + /** + * Not allowed in surrogate pairs + */ + SurrogatePairs = 1, +} + export class TextModel extends Disposable implements model.ITextModel { private static readonly MODEL_SYNC_LIMIT = 50 * 1024 * 1024; // 50 MB @@ -673,7 +684,7 @@ export class TextModel extends Disposable implements model.ITextModel { public getOffsetAt(rawPosition: IPosition): number { this._assertNotDisposed(); - let position = this._validatePosition(rawPosition.lineNumber, rawPosition.column, false); + let position = this._validatePosition(rawPosition.lineNumber, rawPosition.column, StringOffsetValidationType.Relaxed); return this._buffer.getOffsetAt(position.lineNumber, position.column); } @@ -868,10 +879,7 @@ export class TextModel extends Disposable implements model.ITextModel { return new Range(startLineNumber, startColumn, endLineNumber, endColumn); } - /** - * @param strict Do NOT allow a position inside a high-low surrogate pair - */ - private _isValidPosition(lineNumber: number, column: number, strict: boolean): boolean { + private _isValidPosition(lineNumber: number, column: number, validationType: StringOffsetValidationType): boolean { if (typeof lineNumber !== 'number' || typeof column !== 'number') { return false; } @@ -893,14 +901,19 @@ export class TextModel extends Disposable implements model.ITextModel { return false; } + if (column === 1) { + return true; + } + const maxColumn = this.getLineMaxColumn(lineNumber); if (column > maxColumn) { return false; } - if (strict) { - const [charStartOffset,] = strings.getCharContainingOffset(this._buffer.getLineContent(lineNumber), column - 1); - if (column !== charStartOffset + 1) { + if (validationType === StringOffsetValidationType.SurrogatePairs) { + // !!At this point, column > 1 + const charCodeBefore = this._buffer.getLineCharCode(lineNumber, column - 2); + if (strings.isHighSurrogate(charCodeBefore)) { return false; } } @@ -908,10 +921,7 @@ export class TextModel extends Disposable implements model.ITextModel { return true; } - /** - * @param strict Do NOT allow a position inside a high-low surrogate pair - */ - private _validatePosition(_lineNumber: number, _column: number, strict: boolean): Position { + private _validatePosition(_lineNumber: number, _column: number, validationType: StringOffsetValidationType): Position { const lineNumber = Math.floor((typeof _lineNumber === 'number' && !isNaN(_lineNumber)) ? _lineNumber : 1); const column = Math.floor((typeof _column === 'number' && !isNaN(_column)) ? _column : 1); const lineCount = this._buffer.getLineCount(); @@ -933,10 +943,13 @@ export class TextModel extends Disposable implements model.ITextModel { return new Position(lineNumber, maxColumn); } - if (strict) { - const [charStartOffset,] = strings.getCharContainingOffset(this._buffer.getLineContent(lineNumber), column - 1); - if (column !== charStartOffset + 1) { - return new Position(lineNumber, charStartOffset + 1); + if (validationType === StringOffsetValidationType.SurrogatePairs) { + // If the position would end up in the middle of a high-low surrogate pair, + // we move it to before the pair + // !!At this point, column > 1 + const charCodeBefore = this._buffer.getLineCharCode(lineNumber, column - 2); + if (strings.isHighSurrogate(charCodeBefore)) { + return new Position(lineNumber, column - 1); } } @@ -944,94 +957,95 @@ export class TextModel extends Disposable implements model.ITextModel { } public validatePosition(position: IPosition): Position { + const validationType = StringOffsetValidationType.SurrogatePairs; this._assertNotDisposed(); // Avoid object allocation and cover most likely case if (position instanceof Position) { - if (this._isValidPosition(position.lineNumber, position.column, true)) { + if (this._isValidPosition(position.lineNumber, position.column, validationType)) { return position; } } - return this._validatePosition(position.lineNumber, position.column, true); + return this._validatePosition(position.lineNumber, position.column, validationType); } - /** - * @param strict Do NOT allow a range to have its boundaries inside a high-low surrogate pair - */ - private _isValidRange(range: Range, strict: boolean): boolean { + private _isValidRange(range: Range, validationType: StringOffsetValidationType): boolean { const startLineNumber = range.startLineNumber; const startColumn = range.startColumn; const endLineNumber = range.endLineNumber; const endColumn = range.endColumn; - if (!this._isValidPosition(startLineNumber, startColumn, false)) { + if (!this._isValidPosition(startLineNumber, startColumn, StringOffsetValidationType.Relaxed)) { return false; } - if (!this._isValidPosition(endLineNumber, endColumn, false)) { + if (!this._isValidPosition(endLineNumber, endColumn, StringOffsetValidationType.Relaxed)) { return false; } - if (strict) { - const startLineContent = this._buffer.getLineContent(startLineNumber); - if (startColumn < startLineContent.length + 1) { - const [charStartOffset,] = strings.getCharContainingOffset(startLineContent, startColumn - 1); - if (startColumn !== charStartOffset + 1) { - return false; - } - } + if (validationType === StringOffsetValidationType.SurrogatePairs) { + const charCodeBeforeStart = (startColumn > 1 ? this._buffer.getLineCharCode(startLineNumber, startColumn - 2) : 0); + const charCodeBeforeEnd = (endColumn > 1 && endColumn <= this._buffer.getLineLength(endLineNumber) ? this._buffer.getLineCharCode(endLineNumber, endColumn - 2) : 0); - if (endColumn >= 2) { - const endLineContent = (endLineNumber === startLineNumber ? startLineContent : this._buffer.getLineContent(endLineNumber)); - const [, charEndOffset] = strings.getCharContainingOffset(endLineContent, endColumn - 2); - if (endColumn !== charEndOffset + 1) { - return false; - } - } + const startInsideSurrogatePair = strings.isHighSurrogate(charCodeBeforeStart); + const endInsideSurrogatePair = strings.isHighSurrogate(charCodeBeforeEnd); - return true; + if (!startInsideSurrogatePair && !endInsideSurrogatePair) { + return true; + } + return false; } return true; } public validateRange(_range: IRange): Range { + const validationType = StringOffsetValidationType.SurrogatePairs; this._assertNotDisposed(); // Avoid object allocation and cover most likely case if ((_range instanceof Range) && !(_range instanceof Selection)) { - if (this._isValidRange(_range, true)) { + if (this._isValidRange(_range, validationType)) { return _range; } } - const start = this._validatePosition(_range.startLineNumber, _range.startColumn, false); - const end = this._validatePosition(_range.endLineNumber, _range.endColumn, false); + const start = this._validatePosition(_range.startLineNumber, _range.startColumn, StringOffsetValidationType.Relaxed); + const end = this._validatePosition(_range.endLineNumber, _range.endColumn, StringOffsetValidationType.Relaxed); const startLineNumber = start.lineNumber; - let startColumn = start.column; + const startColumn = start.column; const endLineNumber = end.lineNumber; - let endColumn = end.column; - const isEmpty = (startLineNumber === endLineNumber && startColumn === endColumn); + const endColumn = end.column; - const startLineContent = this._buffer.getLineContent(startLineNumber); - if (startColumn < startLineContent.length + 1) { - const [charStartOffset,] = strings.getCharContainingOffset(startLineContent, startColumn - 1); - if (startColumn !== charStartOffset + 1) { - if (isEmpty) { - // do not expand a collapsed range, simply move it to a valid location - return new Range(startLineNumber, charStartOffset + 1, startLineNumber, charStartOffset + 1); - } - startColumn = charStartOffset + 1; - } - } + if (validationType === StringOffsetValidationType.SurrogatePairs) { + const charCodeBeforeStart = (startColumn > 1 ? this._buffer.getLineCharCode(startLineNumber, startColumn - 2) : 0); + const charCodeBeforeEnd = (endColumn > 1 && endColumn <= this._buffer.getLineLength(endLineNumber) ? this._buffer.getLineCharCode(endLineNumber, endColumn - 2) : 0); - if (endColumn >= 2) { - const endLineContent = (endLineNumber === startLineNumber ? startLineContent : this._buffer.getLineContent(endLineNumber)); - const [, charEndOffset] = strings.getCharContainingOffset(endLineContent, endColumn - 2); - if (endColumn !== charEndOffset + 1) { - endColumn = charEndOffset + 1; + const startInsideSurrogatePair = strings.isHighSurrogate(charCodeBeforeStart); + const endInsideSurrogatePair = strings.isHighSurrogate(charCodeBeforeEnd); + + if (!startInsideSurrogatePair && !endInsideSurrogatePair) { + return new Range(startLineNumber, startColumn, endLineNumber, endColumn); } + + if (startLineNumber === endLineNumber && startColumn === endColumn) { + // do not expand a collapsed range, simply move it to a valid location + return new Range(startLineNumber, startColumn - 1, endLineNumber, endColumn - 1); + } + + if (startInsideSurrogatePair && endInsideSurrogatePair) { + // expand range at both ends + return new Range(startLineNumber, startColumn - 1, endLineNumber, endColumn + 1); + } + + if (startInsideSurrogatePair) { + // only expand range at the start + return new Range(startLineNumber, startColumn - 1, endLineNumber, endColumn); + } + + // only expand range at the end + return new Range(startLineNumber, startColumn, endLineNumber, endColumn + 1); } return new Range(startLineNumber, startColumn, endLineNumber, endColumn); diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 3c1f922a9b..67a2ac73d0 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -481,6 +481,7 @@ export interface CompletionItem { export interface CompletionList { suggestions: CompletionItem[]; incomplete?: boolean; + isDetailsResolved?: boolean; dispose?(): void; } @@ -554,8 +555,8 @@ export interface CodeAction { /** * @internal */ -export const enum CodeActionTrigger { - Automatic = 1, +export const enum CodeActionTriggerType { + Auto = 1, Manual = 2, } @@ -564,7 +565,7 @@ export const enum CodeActionTrigger { */ export interface CodeActionContext { only?: string; - trigger: CodeActionTrigger; + trigger: CodeActionTriggerType; } export interface CodeActionList extends IDisposable { @@ -586,6 +587,11 @@ export interface CodeActionProvider { * Optional list of CodeActionKinds that this provider returns. */ providedCodeActionKinds?: ReadonlyArray; + + /** + * @internal + */ + _getAdditionalMenuItems?(context: CodeActionContext, actions: readonly CodeAction[]): Command[]; } /** @@ -1234,31 +1240,41 @@ export class FoldingRangeKind { /** * @internal */ -export function isResourceFileEdit(thing: any): thing is ResourceFileEdit { - return isObject(thing) && (Boolean((thing).newUri) || Boolean((thing).oldUri)); +export namespace WorkspaceFileEdit { + /** + * @internal + */ + export function is(thing: any): thing is WorkspaceFileEdit { + return isObject(thing) && (Boolean((thing).newUri) || Boolean((thing).oldUri)); + } } /** * @internal */ -export function isResourceTextEdit(thing: any): thing is ResourceTextEdit { - return isObject(thing) && (thing).resource && Array.isArray((thing).edits); +export namespace WorkspaceTextEdit { + /** + * @internal + */ + export function is(thing: any): thing is WorkspaceTextEdit { + return isObject(thing) && (thing).resource && Array.isArray((thing).edits); + } } -export interface ResourceFileEdit { +export interface WorkspaceFileEdit { oldUri?: URI; newUri?: URI; options?: { overwrite?: boolean, ignoreIfNotExists?: boolean, ignoreIfExists?: boolean, recursive?: boolean }; } -export interface ResourceTextEdit { +export interface WorkspaceTextEdit { resource: URI; modelVersionId?: number; edits: TextEdit[]; } export interface WorkspaceEdit { - edits: Array; + edits: Array; } export interface Rejection { @@ -1274,6 +1290,14 @@ export interface RenameProvider { resolveRenameLocation?(model: model.ITextModel, position: Position, token: CancellationToken): ProviderResult; } +/** + * @internal + */ +export interface Session { + id: string; + accessToken: string; + displayName: string; +} export interface Command { id: string; @@ -1486,10 +1510,15 @@ export interface SemanticTokensEdits { readonly edits: SemanticTokensEdit[]; } -export interface SemanticTokensProvider { +export interface DocumentSemanticTokensProvider { getLegend(): SemanticTokensLegend; - provideSemanticTokens(model: model.ITextModel, lastResultId: string | null, ranges: Range[] | null, token: CancellationToken): ProviderResult; - releaseSemanticTokens(resultId: string | undefined): void; + provideDocumentSemanticTokens(model: model.ITextModel, lastResultId: string | null, token: CancellationToken): ProviderResult; + releaseDocumentSemanticTokens(resultId: string | undefined): void; +} + +export interface DocumentRangeSemanticTokensProvider { + getLegend(): SemanticTokensLegend; + provideDocumentRangeSemanticTokens(model: model.ITextModel, range: Range, token: CancellationToken): ProviderResult; } // --- feature registries ------ @@ -1597,7 +1626,12 @@ export const FoldingRangeProviderRegistry = new LanguageFeatureRegistry(); +export const DocumentSemanticTokensProviderRegistry = new LanguageFeatureRegistry(); + +/** + * @internal + */ +export const DocumentRangeSemanticTokensProviderRegistry = new LanguageFeatureRegistry(); /** * @internal diff --git a/src/vs/editor/common/modes/languageConfigurationRegistry.ts b/src/vs/editor/common/modes/languageConfigurationRegistry.ts index 477fb1f41e..151cd64d0a 100644 --- a/src/vs/editor/common/modes/languageConfigurationRegistry.ts +++ b/src/vs/editor/common/modes/languageConfigurationRegistry.ts @@ -153,7 +153,7 @@ export class LanguageConfigurationChangeEvent { export class LanguageConfigurationRegistryImpl { - private readonly _entries = new Map(); + private readonly _entries = new Map(); private readonly _onDidChange = new Emitter(); public readonly onDidChange: Event = this._onDidChange.event; diff --git a/src/vs/editor/common/services/editorSimpleWorker.ts b/src/vs/editor/common/services/editorSimpleWorker.ts index 1a4eb9d349..df66955201 100644 --- a/src/vs/editor/common/services/editorSimpleWorker.ts +++ b/src/vs/editor/common/services/editorSimpleWorker.ts @@ -13,7 +13,7 @@ import { IRequestHandler } from 'vs/base/common/worker/simpleWorker'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { DiffComputer } from 'vs/editor/common/diff/diffComputer'; -import * as editorCommon from 'vs/editor/common/editorCommon'; +import { IChange } from 'vs/editor/common/editorCommon'; import { EndOfLineSequence, IWordAtPosition } from 'vs/editor/common/model'; import { IModelChangedEvent, MirrorTextModel as BaseMirrorModel } from 'vs/editor/common/model/mirrorTextModel'; import { ensureValidWordDefinition, getWordAtText } from 'vs/editor/common/model/wordHelper'; @@ -322,7 +322,7 @@ export interface IForeignModuleFactory { (ctx: IWorkerContext, createData: any): any; } -declare var require: any; +declare const require: any; /** * @internal @@ -419,7 +419,7 @@ export class EditorSimpleWorker implements IRequestHandler, IDisposable { return true; } - public async computeDirtyDiff(originalUrl: string, modifiedUrl: string, ignoreTrimWhitespace: boolean): Promise { + public async computeDirtyDiff(originalUrl: string, modifiedUrl: string, ignoreTrimWhitespace: boolean): Promise { let original = this._getModel(originalUrl); let modified = this._getModel(modifiedUrl); if (!original || !modified) { diff --git a/src/vs/editor/common/services/editorWorkerServiceImpl.ts b/src/vs/editor/common/services/editorWorkerServiceImpl.ts index 9f7fd38758..ce61d1d056 100644 --- a/src/vs/editor/common/services/editorWorkerServiceImpl.ts +++ b/src/vs/editor/common/services/editorWorkerServiceImpl.ts @@ -10,7 +10,7 @@ import { SimpleWorkerClient, logOnceWebWorkerWarning, IWorkerClient } from 'vs/b import { DefaultWorkerFactory } from 'vs/base/worker/defaultWorkerFactory'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; -import * as editorCommon from 'vs/editor/common/editorCommon'; +import { IChange } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; import * as modes from 'vs/editor/common/modes'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; @@ -90,7 +90,7 @@ export class EditorWorkerServiceImpl extends Disposable implements IEditorWorker return (canSyncModel(this._modelService, original) && canSyncModel(this._modelService, modified)); } - public computeDirtyDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean): Promise { + public computeDirtyDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean): Promise { return this._workerManager.withWorker().then(client => client.computeDirtyDiff(original, modified, ignoreTrimWhitespace)); } @@ -437,7 +437,7 @@ export class EditorWorkerClient extends Disposable { }); } - public computeDirtyDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean): Promise { + public computeDirtyDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean): Promise { return this._withSyncedResources([original, modified]).then(proxy => { return proxy.computeDirtyDiff(original.toString(), modified.toString(), ignoreTrimWhitespace); }); diff --git a/src/vs/editor/common/services/markerDecorationsServiceImpl.ts b/src/vs/editor/common/services/markerDecorationsServiceImpl.ts index 625ebc08ca..f1bc5504b1 100644 --- a/src/vs/editor/common/services/markerDecorationsServiceImpl.ts +++ b/src/vs/editor/common/services/markerDecorationsServiceImpl.ts @@ -38,7 +38,9 @@ class MarkerDecorations extends Disposable { } public update(markers: IMarker[], newDecorations: IModelDeltaDecoration[]): void { - const ids = this.model.deltaDecorations(keys(this._markersData), newDecorations); + const oldIds = keys(this._markersData); + this._markersData.clear(); + const ids = this.model.deltaDecorations(oldIds, newDecorations); for (let index = 0; index < ids.length; index++) { this._markersData.set(ids[index], markers[index]); } diff --git a/src/vs/editor/common/services/modelServiceImpl.ts b/src/vs/editor/common/services/modelServiceImpl.ts index 1aa0df8769..b8243ef9c7 100644 --- a/src/vs/editor/common/services/modelServiceImpl.ts +++ b/src/vs/editor/common/services/modelServiceImpl.ts @@ -8,13 +8,13 @@ import { Disposable, IDisposable, DisposableStore } from 'vs/base/common/lifecyc import * as platform from 'vs/base/common/platform'; import * as errors from 'vs/base/common/errors'; import { URI } from 'vs/base/common/uri'; -import { EDITOR_MODEL_DEFAULTS } from 'vs/editor/common/config/editorOptions'; +import { EDITOR_MODEL_DEFAULTS, IEditorSemanticHighlightingOptions } from 'vs/editor/common/config/editorOptions'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; import { DefaultEndOfLine, EndOfLinePreference, EndOfLineSequence, IIdentifiedSingleEditOperation, ITextBuffer, ITextBufferFactory, ITextModel, ITextModelCreationOptions } from 'vs/editor/common/model'; import { TextModel, createTextBuffer } from 'vs/editor/common/model/textModel'; import { IModelLanguageChangedEvent, IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; -import { LanguageIdentifier, SemanticTokensProviderRegistry, SemanticTokensProvider, SemanticTokensLegend, SemanticTokens, SemanticTokensEdits, TokenMetadata } from 'vs/editor/common/modes'; +import { LanguageIdentifier, DocumentSemanticTokensProviderRegistry, DocumentSemanticTokensProvider, SemanticTokensLegend, SemanticTokens, SemanticTokensEdits, TokenMetadata } from 'vs/editor/common/modes'; import { PLAINTEXT_LANGUAGE_IDENTIFIER } from 'vs/editor/common/modes/modesRegistry'; import { ILanguageSelection } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; @@ -133,7 +133,7 @@ export class ModelServiceImpl extends Disposable implements IModelService { this._configurationServiceSubscription = this._configurationService.onDidChangeConfiguration(e => this._updateModelOptions()); this._updateModelOptions(); - this._register(new SemanticColoringFeature(this, themeService, logService)); + this._register(new SemanticColoringFeature(this, themeService, configurationService, logService)); } private static _readModelOptions(config: IRawConfig, isForSimpleWidget: boolean): ITextModelCreationOptions { @@ -442,42 +442,79 @@ export interface ILineSequence { } class SemanticColoringFeature extends Disposable { + + private static readonly SETTING_ID = 'editor.semanticHighlighting'; + private _watchers: Record; private _semanticStyling: SemanticStyling; + private _configurationService: IConfigurationService; - constructor(modelService: IModelService, themeService: IThemeService, logService: ILogService) { + constructor(modelService: IModelService, themeService: IThemeService, configurationService: IConfigurationService, logService: ILogService) { super(); + this._configurationService = configurationService; this._watchers = Object.create(null); this._semanticStyling = this._register(new SemanticStyling(themeService, logService)); - this._register(modelService.onModelAdded((model) => { + + const isSemanticColoringEnabled = (model: ITextModel) => { + const options = configurationService.getValue(SemanticColoringFeature.SETTING_ID, { overrideIdentifier: model.getLanguageIdentifier().language, resource: model.uri }); + return options && options.enabled; + }; + const register = (model: ITextModel) => { this._watchers[model.uri.toString()] = new ModelSemanticColoring(model, themeService, this._semanticStyling); + }; + const deregister = (model: ITextModel, modelSemanticColoring: ModelSemanticColoring) => { + modelSemanticColoring.dispose(); + delete this._watchers[model.uri.toString()]; + }; + this._register(modelService.onModelAdded((model) => { + if (isSemanticColoringEnabled(model)) { + register(model); + } })); this._register(modelService.onModelRemoved((model) => { - this._watchers[model.uri.toString()].dispose(); - delete this._watchers[model.uri.toString()]; + const curr = this._watchers[model.uri.toString()]; + if (curr) { + deregister(model, curr); + } })); + this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(SemanticColoringFeature.SETTING_ID)) { + for (let model of modelService.getModels()) { + const curr = this._watchers[model.uri.toString()]; + if (isSemanticColoringEnabled(model)) { + if (!curr) { + register(model); + } + } else { + if (curr) { + deregister(model, curr); + } + } + } + } + }); } } class SemanticStyling extends Disposable { - private _caches: WeakMap; + private _caches: WeakMap; constructor( private readonly _themeService: IThemeService, private readonly _logService: ILogService ) { super(); - this._caches = new WeakMap(); + this._caches = new WeakMap(); if (this._themeService) { // workaround for tests which use undefined... :/ this._register(this._themeService.onThemeChange(() => { - this._caches = new WeakMap(); + this._caches = new WeakMap(); })); } } - public get(provider: SemanticTokensProvider): SemanticColoringProviderStyling { + public get(provider: DocumentSemanticTokensProvider): SemanticColoringProviderStyling { if (!this._caches.has(provider)) { this._caches.set(provider, new SemanticColoringProviderStyling(provider.getLegend(), this._themeService, this._logService)); } @@ -598,11 +635,12 @@ class SemanticColoringProviderStyling { } else { const tokenType = this._legend.tokenTypes[tokenTypeIndex]; const tokenModifiers: string[] = []; - for (let modifierIndex = 0; tokenModifierSet !== 0 && modifierIndex < this._legend.tokenModifiers.length; modifierIndex++) { - if (tokenModifierSet & 1) { + let modifierSet = tokenModifierSet; + for (let modifierIndex = 0; modifierSet > 0 && modifierIndex < this._legend.tokenModifiers.length; modifierIndex++) { + if (modifierSet & 1) { tokenModifiers.push(this._legend.tokenModifiers[modifierIndex]); } - tokenModifierSet = tokenModifierSet >> 1; + modifierSet = modifierSet >> 1; } metadata = this._themeService.getTheme().getTokenStyleMetadata(tokenType, tokenModifiers); @@ -638,13 +676,13 @@ const enum SemanticColoringConstants { class SemanticTokensResponse { constructor( - private readonly _provider: SemanticTokensProvider, + private readonly _provider: DocumentSemanticTokensProvider, public readonly resultId: string | undefined, public readonly data: Uint32Array ) { } public dispose(): void { - this._provider.releaseSemanticTokens(this.resultId); + this._provider.releaseDocumentSemanticTokens(this.resultId); } } @@ -672,7 +710,7 @@ class ModelSemanticColoring extends Disposable { this._fetchSemanticTokens.schedule(); } })); - this._register(SemanticTokensProviderRegistry.onDidChange(e => this._fetchSemanticTokens.schedule())); + this._register(DocumentSemanticTokensProviderRegistry.onDidChange(e => this._fetchSemanticTokens.schedule())); if (themeService) { // workaround for tests which use undefined... :/ this._register(themeService.onThemeChange(_ => { @@ -685,7 +723,6 @@ class ModelSemanticColoring extends Disposable { } public dispose(): void { - this._isDisposed = true; if (this._currentResponse) { this._currentResponse.dispose(); this._currentResponse = null; @@ -694,6 +731,9 @@ class ModelSemanticColoring extends Disposable { this._currentRequestCancellationTokenSource.cancel(); this._currentRequestCancellationTokenSource = null; } + this._setSemanticTokens(null, null, null, []); + this._isDisposed = true; + super.dispose(); } @@ -716,7 +756,7 @@ class ModelSemanticColoring extends Disposable { const styling = this._semanticStyling.get(provider); const lastResultId = this._currentResponse ? this._currentResponse.resultId || null : null; - const request = Promise.resolve(provider.provideSemanticTokens(this._model, lastResultId, null, this._currentRequestCancellationTokenSource.token)); + const request = Promise.resolve(provider.provideDocumentSemanticTokens(this._model, lastResultId, this._currentRequestCancellationTokenSource.token)); request.then((res) => { this._currentRequestCancellationTokenSource = null; @@ -744,7 +784,7 @@ class ModelSemanticColoring extends Disposable { } } - private _setSemanticTokens(provider: SemanticTokensProvider | null, tokens: SemanticTokens | SemanticTokensEdits | null, styling: SemanticColoringProviderStyling | null, pendingChanges: IModelContentChangedEvent[]): void { + private _setSemanticTokens(provider: DocumentSemanticTokensProvider | null, tokens: SemanticTokens | SemanticTokensEdits | null, styling: SemanticColoringProviderStyling | null, pendingChanges: IModelContentChangedEvent[]): void { const currentResponse = this._currentResponse; if (this._currentResponse) { this._currentResponse.dispose(); @@ -753,7 +793,7 @@ class ModelSemanticColoring extends Disposable { if (this._isDisposed) { // disposed! if (provider && tokens) { - provider.releaseSemanticTokens(tokens.resultId); + provider.releaseDocumentSemanticTokens(tokens.resultId); } return; } @@ -914,8 +954,8 @@ class ModelSemanticColoring extends Disposable { this._model.setSemanticTokens(null); } - private _getSemanticColoringProvider(): SemanticTokensProvider | null { - const result = SemanticTokensProviderRegistry.ordered(this._model); + private _getSemanticColoringProvider(): DocumentSemanticTokensProvider | null { + const result = DocumentSemanticTokensProviderRegistry.ordered(this._model); return (result.length > 0 ? result[0] : null); } } diff --git a/src/vs/editor/common/services/resolverService.ts b/src/vs/editor/common/services/resolverService.ts index 588ec89aba..7d4de346ef 100644 --- a/src/vs/editor/common/services/resolverService.ts +++ b/src/vs/editor/common/services/resolverService.ts @@ -53,9 +53,14 @@ export interface ITextEditorModel extends IEditorModel { createSnapshot(this: ITextEditorModel): ITextSnapshot | null; /** - * Signals if this model is readonly or not. + * Signals if this model is readonly or not. */ isReadonly(): boolean; + + /** + * Figure out if this model is resolved or not. + */ + isResolved(): this is IResolvedTextEditorModel; } export interface IResolvedTextEditorModel extends ITextEditorModel { diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index 7f6e1b33b4..9404a4beee 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -6,16 +6,329 @@ // THIS IS A GENERATED FILE. DO NOT EDIT DIRECTLY. -export enum MarkerTag { - Unnecessary = 1, - Deprecated = 2 +export enum AccessibilitySupport { + /** + * This should be the browser case where it is not known if a screen reader is attached or no. + */ + Unknown = 0, + Disabled = 1, + Enabled = 2 } -export enum MarkerSeverity { - Hint = 1, - Info = 2, - Warning = 4, - Error = 8 +export enum CompletionItemInsertTextRule { + /** + * Adjust whitespace/indentation of multiline insert texts to + * match the current line indentation. + */ + KeepWhitespace = 1, + /** + * `insertText` is a snippet. + */ + InsertAsSnippet = 4 +} + +export enum CompletionItemKind { + Method = 0, + Function = 1, + Constructor = 2, + Field = 3, + Variable = 4, + Class = 5, + Struct = 6, + Interface = 7, + Module = 8, + Property = 9, + Event = 10, + Operator = 11, + Unit = 12, + Value = 13, + Constant = 14, + Enum = 15, + EnumMember = 16, + Keyword = 17, + Text = 18, + Color = 19, + File = 20, + Reference = 21, + Customcolor = 22, + Folder = 23, + TypeParameter = 24, + Snippet = 25 +} + +export enum CompletionItemTag { + Deprecated = 1 +} + +/** + * How a suggest provider was triggered. + */ +export enum CompletionTriggerKind { + Invoke = 0, + TriggerCharacter = 1, + TriggerForIncompleteCompletions = 2 +} + +/** + * A positioning preference for rendering content widgets. + */ +export enum ContentWidgetPositionPreference { + /** + * Place the content widget exactly at a position + */ + EXACT = 0, + /** + * Place the content widget above a position + */ + ABOVE = 1, + /** + * Place the content widget below a position + */ + BELOW = 2 +} + +/** + * Describes the reason the cursor has changed its position. + */ +export enum CursorChangeReason { + /** + * Unknown or not set. + */ + NotSet = 0, + /** + * A `model.setValue()` was called. + */ + ContentFlush = 1, + /** + * The `model` has been changed outside of this cursor and the cursor recovers its position from associated markers. + */ + RecoverFromMarkers = 2, + /** + * There was an explicit user gesture. + */ + Explicit = 3, + /** + * There was a Paste. + */ + Paste = 4, + /** + * There was an Undo. + */ + Undo = 5, + /** + * There was a Redo. + */ + Redo = 6 +} + +/** + * The default end of line to use when instantiating models. + */ +export enum DefaultEndOfLine { + /** + * Use line feed (\n) as the end of line character. + */ + LF = 1, + /** + * Use carriage return and line feed (\r\n) as the end of line character. + */ + CRLF = 2 +} + +/** + * A document highlight kind. + */ +export enum DocumentHighlightKind { + /** + * A textual occurrence. + */ + Text = 0, + /** + * Read-access of a symbol, like reading a variable. + */ + Read = 1, + /** + * Write-access of a symbol, like writing to a variable. + */ + Write = 2 +} + +/** + * Configuration options for auto indentation in the editor + */ +export enum EditorAutoIndentStrategy { + None = 0, + Keep = 1, + Brackets = 2, + Advanced = 3, + Full = 4 +} + +export enum EditorOption { + acceptSuggestionOnCommitCharacter = 0, + acceptSuggestionOnEnter = 1, + accessibilitySupport = 2, + accessibilityPageSize = 3, + ariaLabel = 4, + autoClosingBrackets = 5, + autoClosingOvertype = 6, + autoClosingQuotes = 7, + autoIndent = 8, + automaticLayout = 9, + autoSurround = 10, + codeLens = 11, + colorDecorators = 12, + contextmenu = 13, + copyWithSyntaxHighlighting = 14, + cursorBlinking = 15, + cursorSmoothCaretAnimation = 16, + cursorStyle = 17, + cursorSurroundingLines = 18, + cursorSurroundingLinesStyle = 19, + cursorWidth = 20, + disableLayerHinting = 21, + disableMonospaceOptimizations = 22, + dragAndDrop = 23, + emptySelectionClipboard = 24, + extraEditorClassName = 25, + fastScrollSensitivity = 26, + find = 27, + fixedOverflowWidgets = 28, + folding = 29, + foldingStrategy = 30, + foldingHighlight = 31, + fontFamily = 32, + fontInfo = 33, + fontLigatures = 34, + fontSize = 35, + fontWeight = 36, + formatOnPaste = 37, + formatOnType = 38, + glyphMargin = 39, + gotoLocation = 40, + hideCursorInOverviewRuler = 41, + highlightActiveIndentGuide = 42, + hover = 43, + inDiffEditor = 44, + letterSpacing = 45, + lightbulb = 46, + lineDecorationsWidth = 47, + lineHeight = 48, + lineNumbers = 49, + lineNumbersMinChars = 50, + links = 51, + matchBrackets = 52, + minimap = 53, + mouseStyle = 54, + mouseWheelScrollSensitivity = 55, + mouseWheelZoom = 56, + multiCursorMergeOverlapping = 57, + multiCursorModifier = 58, + multiCursorPaste = 59, + occurrencesHighlight = 60, + overviewRulerBorder = 61, + overviewRulerLanes = 62, + parameterHints = 63, + peekWidgetFocusInlineEditor = 64, + quickSuggestions = 65, + quickSuggestionsDelay = 66, + readOnly = 67, + renderControlCharacters = 68, + renderIndentGuides = 69, + renderFinalNewline = 70, + renderLineHighlight = 71, + renderWhitespace = 72, + revealHorizontalRightPadding = 73, + roundedSelection = 74, + rulers = 75, + scrollbar = 76, + scrollBeyondLastColumn = 77, + scrollBeyondLastLine = 78, + selectionClipboard = 79, + selectionHighlight = 80, + selectOnLineNumbers = 81, + semanticHighlighting = 82, + showFoldingControls = 83, + showUnused = 84, + snippetSuggestions = 85, + smoothScrolling = 86, + stopRenderingLineAfter = 87, + suggest = 88, + suggestFontSize = 89, + suggestLineHeight = 90, + suggestOnTriggerCharacters = 91, + suggestSelection = 92, + tabCompletion = 93, + useTabStops = 94, + wordSeparators = 95, + wordWrap = 96, + wordWrapBreakAfterCharacters = 97, + wordWrapBreakBeforeCharacters = 98, + wordWrapColumn = 99, + wordWrapMinified = 100, + wrappingIndent = 101, + wrappingAlgorithm = 102, + editorClassName = 103, + pixelRatio = 104, + tabFocusMode = 105, + layoutInfo = 106, + wrappingInfo = 107 +} + +/** + * End of line character preference. + */ +export enum EndOfLinePreference { + /** + * Use the end of line character identified in the text buffer. + */ + TextDefined = 0, + /** + * Use line feed (\n) as the end of line character. + */ + LF = 1, + /** + * Use carriage return and line feed (\r\n) as the end of line character. + */ + CRLF = 2 +} + +/** + * End of line character preference. + */ +export enum EndOfLineSequence { + /** + * Use line feed (\n) as the end of line character. + */ + LF = 0, + /** + * Use carriage return and line feed (\r\n) as the end of line character. + */ + CRLF = 1 +} + +/** + * Describes what to do with the indentation when pressing Enter. + */ +export enum IndentAction { + /** + * Insert new line and copy the previous line's indentation. + */ + None = 0, + /** + * Insert new line and indent once (relative to the previous line's indentation). + */ + Indent = 1, + /** + * Insert two new lines: + * - the first one indented which will hold the cursor + * - the second one at the same indentation level + */ + IndentOutdent = 2, + /** + * Insert new line and outdent once (relative to the previous line's indentation). + */ + Outdent = 3 } /** @@ -199,34 +512,16 @@ export enum KeyCode { MAX_VALUE = 112 } -/** - * The direction of a selection. - */ -export enum SelectionDirection { - /** - * The selection starts above where it ends. - */ - LTR = 0, - /** - * The selection starts below where it ends. - */ - RTL = 1 +export enum MarkerSeverity { + Hint = 1, + Info = 2, + Warning = 4, + Error = 8 } -export enum ScrollbarVisibility { - Auto = 1, - Hidden = 2, - Visible = 3 -} - -/** - * Vertical Lane in the overview ruler of the editor. - */ -export enum OverviewRulerLane { - Left = 1, - Center = 2, - Right = 4, - Full = 7 +export enum MarkerTag { + Unnecessary = 1, + Deprecated = 2 } /** @@ -237,144 +532,6 @@ export enum MinimapPosition { Gutter = 2 } -/** - * End of line character preference. - */ -export enum EndOfLinePreference { - /** - * Use the end of line character identified in the text buffer. - */ - TextDefined = 0, - /** - * Use line feed (\n) as the end of line character. - */ - LF = 1, - /** - * Use carriage return and line feed (\r\n) as the end of line character. - */ - CRLF = 2 -} - -/** - * The default end of line to use when instantiating models. - */ -export enum DefaultEndOfLine { - /** - * Use line feed (\n) as the end of line character. - */ - LF = 1, - /** - * Use carriage return and line feed (\r\n) as the end of line character. - */ - CRLF = 2 -} - -/** - * End of line character preference. - */ -export enum EndOfLineSequence { - /** - * Use line feed (\n) as the end of line character. - */ - LF = 0, - /** - * Use carriage return and line feed (\r\n) as the end of line character. - */ - CRLF = 1 -} - -/** - * Describes the behavior of decorations when typing/editing near their edges. - * Note: Please do not edit the values, as they very carefully match `DecorationRangeBehavior` - */ -export enum TrackedRangeStickiness { - AlwaysGrowsWhenTypingAtEdges = 0, - NeverGrowsWhenTypingAtEdges = 1, - GrowsOnlyWhenTypingBefore = 2, - GrowsOnlyWhenTypingAfter = 3 -} - -export enum ScrollType { - Smooth = 0, - Immediate = 1 -} - -/** - * Describes the reason the cursor has changed its position. - */ -export enum CursorChangeReason { - /** - * Unknown or not set. - */ - NotSet = 0, - /** - * A `model.setValue()` was called. - */ - ContentFlush = 1, - /** - * The `model` has been changed outside of this cursor and the cursor recovers its position from associated markers. - */ - RecoverFromMarkers = 2, - /** - * There was an explicit user gesture. - */ - Explicit = 3, - /** - * There was a Paste. - */ - Paste = 4, - /** - * There was an Undo. - */ - Undo = 5, - /** - * There was a Redo. - */ - Redo = 6 -} - -export enum RenderMinimap { - None = 0, - Text = 1, - Blocks = 2 -} - -/** - * A positioning preference for rendering content widgets. - */ -export enum ContentWidgetPositionPreference { - /** - * Place the content widget exactly at a position - */ - EXACT = 0, - /** - * Place the content widget above a position - */ - ABOVE = 1, - /** - * Place the content widget below a position - */ - BELOW = 2 -} - -/** - * A positioning preference for rendering overlay widgets. - */ -export enum OverlayWidgetPositionPreference { - /** - * Position the overlay widget in the top right corner - */ - TOP_RIGHT_CORNER = 0, - /** - * Position the overlay widget in the bottom right corner - */ - BOTTOM_RIGHT_CORNER = 1, - /** - * Position the overlay widget in the top center - */ - TOP_CENTER = 2 -} - /** * Type of hit element with the mouse in the editor. */ @@ -438,81 +595,70 @@ export enum MouseTargetType { } /** - * Describes what to do with the indentation when pressing Enter. + * A positioning preference for rendering overlay widgets. */ -export enum IndentAction { +export enum OverlayWidgetPositionPreference { /** - * Insert new line and copy the previous line's indentation. + * Position the overlay widget in the top right corner */ - None = 0, + TOP_RIGHT_CORNER = 0, /** - * Insert new line and indent once (relative to the previous line's indentation). + * Position the overlay widget in the bottom right corner */ - Indent = 1, + BOTTOM_RIGHT_CORNER = 1, /** - * Insert two new lines: - * - the first one indented which will hold the cursor - * - the second one at the same indentation level + * Position the overlay widget in the top center */ - IndentOutdent = 2, - /** - * Insert new line and outdent once (relative to the previous line's indentation). - */ - Outdent = 3 -} - -export enum CompletionItemKind { - Method = 0, - Function = 1, - Constructor = 2, - Field = 3, - Variable = 4, - Class = 5, - Struct = 6, - Interface = 7, - Module = 8, - Property = 9, - Event = 10, - Operator = 11, - Unit = 12, - Value = 13, - Constant = 14, - Enum = 15, - EnumMember = 16, - Keyword = 17, - Text = 18, - Color = 19, - File = 20, - Reference = 21, - Customcolor = 22, - Folder = 23, - TypeParameter = 24, - Snippet = 25 -} - -export enum CompletionItemTag { - Deprecated = 1 -} - -export enum CompletionItemInsertTextRule { - /** - * Adjust whitespace/indentation of multiline insert texts to - * match the current line indentation. - */ - KeepWhitespace = 1, - /** - * `insertText` is a snippet. - */ - InsertAsSnippet = 4 + TOP_CENTER = 2 } /** - * How a suggest provider was triggered. + * Vertical Lane in the overview ruler of the editor. */ -export enum CompletionTriggerKind { - Invoke = 0, - TriggerCharacter = 1, - TriggerForIncompleteCompletions = 2 +export enum OverviewRulerLane { + Left = 1, + Center = 2, + Right = 4, + Full = 7 +} + +export enum RenderLineNumbersType { + Off = 0, + On = 1, + Relative = 2, + Interval = 3, + Custom = 4 +} + +export enum RenderMinimap { + None = 0, + Text = 1, + Blocks = 2 +} + +export enum ScrollType { + Smooth = 0, + Immediate = 1 +} + +export enum ScrollbarVisibility { + Auto = 1, + Hidden = 2, + Visible = 3 +} + +/** + * The direction of a selection. + */ +export enum SelectionDirection { + /** + * The selection starts above where it ends. + */ + LTR = 0, + /** + * The selection starts below where it ends. + */ + RTL = 1 } export enum SignatureHelpTriggerKind { @@ -521,24 +667,6 @@ export enum SignatureHelpTriggerKind { ContentChange = 3 } -/** - * A document highlight kind. - */ -export enum DocumentHighlightKind { - /** - * A textual occurrence. - */ - Text = 0, - /** - * Read-access of a symbol, like reading a variable. - */ - Read = 1, - /** - * Write-access of a symbol, like writing to a variable. - */ - Write = 2 -} - /** * A symbol kind. */ @@ -573,4 +701,97 @@ export enum SymbolKind { export enum SymbolTag { Deprecated = 1 +} + +/** + * The kind of animation in which the editor's cursor should be rendered. + */ +export enum TextEditorCursorBlinkingStyle { + /** + * Hidden + */ + Hidden = 0, + /** + * Blinking + */ + Blink = 1, + /** + * Blinking with smooth fading + */ + Smooth = 2, + /** + * Blinking with prolonged filled state and smooth fading + */ + Phase = 3, + /** + * Expand collapse animation on the y axis + */ + Expand = 4, + /** + * No-Blinking + */ + Solid = 5 +} + +/** + * The style in which the editor's cursor should be rendered. + */ +export enum TextEditorCursorStyle { + /** + * As a vertical line (sitting between two characters). + */ + Line = 1, + /** + * As a block (sitting on top of a character). + */ + Block = 2, + /** + * As a horizontal line (sitting under a character). + */ + Underline = 3, + /** + * As a thin vertical line (sitting between two characters). + */ + LineThin = 4, + /** + * As an outlined block (sitting on top of a character). + */ + BlockOutline = 5, + /** + * As a thin horizontal line (sitting under a character). + */ + UnderlineThin = 6 +} + +/** + * Describes the behavior of decorations when typing/editing near their edges. + * Note: Please do not edit the values, as they very carefully match `DecorationRangeBehavior` + */ +export enum TrackedRangeStickiness { + AlwaysGrowsWhenTypingAtEdges = 0, + NeverGrowsWhenTypingAtEdges = 1, + GrowsOnlyWhenTypingBefore = 2, + GrowsOnlyWhenTypingAfter = 3 +} + +/** + * Describes how to indent wrapped lines. + */ +export enum WrappingIndent { + /** + * No indentation => wrapped lines begin at column 1. + */ + None = 0, + /** + * Same => wrapped lines get the same indentation as the parent. + */ + Same = 1, + /** + * Indent => wrapped lines get +1 indentation toward the parent. + */ + Indent = 2, + /** + * DeepIndent => wrapped lines get +2 indentation toward the parent. + */ + DeepIndent = 3 } \ No newline at end of file diff --git a/src/vs/editor/common/view/viewEvents.ts b/src/vs/editor/common/view/viewEvents.ts index 08562381af..f02b5ac556 100644 --- a/src/vs/editor/common/view/viewEvents.ts +++ b/src/vs/editor/common/view/viewEvents.ts @@ -9,25 +9,26 @@ import { ScrollEvent } from 'vs/base/common/scrollable'; import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; -import { ScrollType } from 'vs/editor/common/editorCommon'; +import { ScrollType, IContentSizeChangedEvent } from 'vs/editor/common/editorCommon'; export const enum ViewEventType { ViewConfigurationChanged = 1, - ViewCursorStateChanged = 2, - ViewDecorationsChanged = 3, - ViewFlushed = 4, - ViewFocusChanged = 5, - ViewLineMappingChanged = 6, - ViewLinesChanged = 7, - ViewLinesDeleted = 8, - ViewLinesInserted = 9, - ViewRevealRangeRequest = 10, - ViewScrollChanged = 11, - ViewTokensChanged = 12, - ViewTokensColorsChanged = 13, - ViewZonesChanged = 14, - ViewThemeChanged = 15, - ViewLanguageConfigurationChanged = 16 + ViewContentSizeChanged = 2, + ViewCursorStateChanged = 3, + ViewDecorationsChanged = 4, + ViewFlushed = 5, + ViewFocusChanged = 6, + ViewLanguageConfigurationChanged = 7, + ViewLineMappingChanged = 8, + ViewLinesChanged = 9, + ViewLinesDeleted = 10, + ViewLinesInserted = 11, + ViewRevealRangeRequest = 12, + ViewScrollChanged = 13, + ViewThemeChanged = 14, + ViewTokensChanged = 15, + ViewTokensColorsChanged = 16, + ViewZonesChanged = 17, } export class ViewConfigurationChangedEvent { @@ -45,6 +46,25 @@ export class ViewConfigurationChangedEvent { } } +export class ViewContentSizeChangedEvent implements IContentSizeChangedEvent { + + public readonly type = ViewEventType.ViewContentSizeChanged; + + public readonly contentWidth: number; + public readonly contentHeight: number; + + public readonly contentWidthChanged: boolean; + public readonly contentHeightChanged: boolean; + + constructor(source: IContentSizeChangedEvent) { + this.contentWidth = source.contentWidth; + this.contentHeight = source.contentHeight; + + this.contentWidthChanged = source.contentWidthChanged; + this.contentHeightChanged = source.contentHeightChanged; + } +} + export class ViewCursorStateChangedEvent { public readonly type = ViewEventType.ViewCursorStateChanged; @@ -88,6 +108,11 @@ export class ViewFocusChangedEvent { } } +export class ViewLanguageConfigurationEvent { + + public readonly type = ViewEventType.ViewLanguageConfigurationChanged; +} + export class ViewLineMappingChangedEvent { public readonly type = ViewEventType.ViewLineMappingChanged; @@ -221,6 +246,11 @@ export class ViewScrollChangedEvent { } } +export class ViewThemeChangedEvent { + + public readonly type = ViewEventType.ViewThemeChanged; +} + export class ViewTokensChangedEvent { public readonly type = ViewEventType.ViewTokensChanged; @@ -241,11 +271,6 @@ export class ViewTokensChangedEvent { } } -export class ViewThemeChangedEvent { - - public readonly type = ViewEventType.ViewThemeChanged; -} - export class ViewTokensColorsChangedEvent { public readonly type = ViewEventType.ViewTokensColorsChanged; @@ -264,28 +289,24 @@ export class ViewZonesChangedEvent { } } -export class ViewLanguageConfigurationEvent { - - public readonly type = ViewEventType.ViewLanguageConfigurationChanged; -} - export type ViewEvent = ( ViewConfigurationChangedEvent + | ViewContentSizeChangedEvent | ViewCursorStateChangedEvent | ViewDecorationsChangedEvent | ViewFlushedEvent | ViewFocusChangedEvent - | ViewLinesChangedEvent + | ViewLanguageConfigurationEvent | ViewLineMappingChangedEvent + | ViewLinesChangedEvent | ViewLinesDeletedEvent | ViewLinesInsertedEvent | ViewRevealRangeRequestEvent | ViewScrollChangedEvent + | ViewThemeChangedEvent | ViewTokensChangedEvent | ViewTokensColorsChangedEvent | ViewZonesChangedEvent - | ViewThemeChangedEvent - | ViewLanguageConfigurationEvent ); export interface IViewEventListener { diff --git a/src/vs/editor/common/viewLayout/viewLayout.ts b/src/vs/editor/common/viewLayout/viewLayout.ts index 79f8a995a7..45c8e3b017 100644 --- a/src/vs/editor/common/viewLayout/viewLayout.ts +++ b/src/vs/editor/common/viewLayout/viewLayout.ts @@ -3,26 +3,159 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event } from 'vs/base/common/event'; +import { Event, Emitter } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { IScrollPosition, ScrollEvent, Scrollable, ScrollbarVisibility } from 'vs/base/common/scrollable'; +import { IScrollPosition, ScrollEvent, Scrollable, ScrollbarVisibility, INewScrollPosition } from 'vs/base/common/scrollable'; import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; -import * as editorCommon from 'vs/editor/common/editorCommon'; +import { IConfiguration, IContentSizeChangedEvent } from 'vs/editor/common/editorCommon'; import { LinesLayout, IEditorWhitespace, IWhitespaceChangeAccessor } from 'vs/editor/common/viewLayout/linesLayout'; import { IPartialViewLinesViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; import { IViewLayout, IViewWhitespaceViewportData, Viewport } from 'vs/editor/common/viewModel/viewModel'; const SMOOTH_SCROLLING_TIME = 125; -export class ViewLayout extends Disposable implements IViewLayout { +class EditorScrollDimensions { - private readonly _configuration: editorCommon.IConfiguration; - private readonly _linesLayout: LinesLayout; + public readonly width: number; + public readonly contentWidth: number; + public readonly scrollWidth: number; + + public readonly height: number; + public readonly contentHeight: number; + public readonly scrollHeight: number; + + constructor( + width: number, + contentWidth: number, + height: number, + contentHeight: number, + ) { + width = width | 0; + contentWidth = contentWidth | 0; + height = height | 0; + contentHeight = contentHeight | 0; + + if (width < 0) { + width = 0; + } + if (contentWidth < 0) { + contentWidth = 0; + } + + if (height < 0) { + height = 0; + } + if (contentHeight < 0) { + contentHeight = 0; + } + + this.width = width; + this.contentWidth = contentWidth; + this.scrollWidth = Math.max(width, contentWidth); + + this.height = height; + this.contentHeight = contentHeight; + this.scrollHeight = Math.max(height, contentHeight); + } + + public equals(other: EditorScrollDimensions): boolean { + return ( + this.width === other.width + && this.contentWidth === other.contentWidth + && this.height === other.height + && this.contentHeight === other.contentHeight + ); + } +} + +class EditorScrollable extends Disposable { + + private readonly _scrollable: Scrollable; + private _dimensions: EditorScrollDimensions; - public readonly scrollable: Scrollable; public readonly onDidScroll: Event; - constructor(configuration: editorCommon.IConfiguration, lineCount: number, scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable) { + private readonly _onDidContentSizeChange = this._register(new Emitter()); + public readonly onDidContentSizeChange: Event = this._onDidContentSizeChange.event; + + constructor(smoothScrollDuration: number, scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable) { + super(); + this._dimensions = new EditorScrollDimensions(0, 0, 0, 0); + this._scrollable = this._register(new Scrollable(smoothScrollDuration, scheduleAtNextAnimationFrame)); + this.onDidScroll = this._scrollable.onScroll; + } + + public getScrollable(): Scrollable { + return this._scrollable; + } + + public setSmoothScrollDuration(smoothScrollDuration: number): void { + this._scrollable.setSmoothScrollDuration(smoothScrollDuration); + } + + public validateScrollPosition(scrollPosition: INewScrollPosition): IScrollPosition { + return this._scrollable.validateScrollPosition(scrollPosition); + } + + public getScrollDimensions(): EditorScrollDimensions { + return this._dimensions; + } + + public setScrollDimensions(dimensions: EditorScrollDimensions): void { + if (this._dimensions.equals(dimensions)) { + return; + } + + const oldDimensions = this._dimensions; + this._dimensions = dimensions; + + this._scrollable.setScrollDimensions({ + width: dimensions.width, + scrollWidth: dimensions.scrollWidth, + height: dimensions.height, + scrollHeight: dimensions.scrollHeight + }); + + const contentWidthChanged = (oldDimensions.contentWidth !== dimensions.contentWidth); + const contentHeightChanged = (oldDimensions.contentHeight !== dimensions.contentHeight); + if (contentWidthChanged || contentHeightChanged) { + this._onDidContentSizeChange.fire({ + contentWidth: dimensions.contentWidth, + contentHeight: dimensions.contentHeight, + + contentWidthChanged: contentWidthChanged, + contentHeightChanged: contentHeightChanged + }); + } + } + + public getFutureScrollPosition(): IScrollPosition { + return this._scrollable.getFutureScrollPosition(); + } + + public getCurrentScrollPosition(): IScrollPosition { + return this._scrollable.getCurrentScrollPosition(); + } + + public setScrollPositionNow(update: INewScrollPosition): void { + this._scrollable.setScrollPositionNow(update); + } + + public setScrollPositionSmooth(update: INewScrollPosition): void { + this._scrollable.setScrollPositionSmooth(update); + } +} + +export class ViewLayout extends Disposable implements IViewLayout { + + private readonly _configuration: IConfiguration; + private readonly _linesLayout: LinesLayout; + + private readonly _scrollable: EditorScrollable; + public readonly onDidScroll: Event; + public readonly onDidContentSizeChange: Event; + + constructor(configuration: IConfiguration, lineCount: number, scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable) { super(); this._configuration = configuration; @@ -31,14 +164,17 @@ export class ViewLayout extends Disposable implements IViewLayout { this._linesLayout = new LinesLayout(lineCount, options.get(EditorOption.lineHeight)); - this.scrollable = this._register(new Scrollable(0, scheduleAtNextAnimationFrame)); + this._scrollable = this._register(new EditorScrollable(0, scheduleAtNextAnimationFrame)); this._configureSmoothScrollDuration(); - this.scrollable.setScrollDimensions({ - width: layoutInfo.contentWidth, - height: layoutInfo.contentHeight - }); - this.onDidScroll = this.scrollable.onScroll; + this._scrollable.setScrollDimensions(new EditorScrollDimensions( + layoutInfo.contentWidth, + 0, + layoutInfo.height, + 0 + )); + this.onDidScroll = this._scrollable.onDidScroll; + this.onDidContentSizeChange = this._scrollable.onDidContentSizeChange; this._updateHeight(); } @@ -47,12 +183,16 @@ export class ViewLayout extends Disposable implements IViewLayout { super.dispose(); } + public getScrollable(): Scrollable { + return this._scrollable.getScrollable(); + } + public onHeightMaybeChanged(): void { this._updateHeight(); } private _configureSmoothScrollDuration(): void { - this.scrollable.setSmoothScrollDuration(this._configuration.options.get(EditorOption.smoothScrolling) ? SMOOTH_SCROLLING_TIME : 0); + this._scrollable.setSmoothScrollDuration(this._configuration.options.get(EditorOption.smoothScrolling) ? SMOOTH_SCROLLING_TIME : 0); } // ---- begin view event handlers @@ -65,16 +205,15 @@ export class ViewLayout extends Disposable implements IViewLayout { if (e.hasChanged(EditorOption.layoutInfo)) { const layoutInfo = options.get(EditorOption.layoutInfo); const width = layoutInfo.contentWidth; - const height = layoutInfo.contentHeight; - const scrollDimensions = this.scrollable.getScrollDimensions(); + const height = layoutInfo.height; + const scrollDimensions = this._scrollable.getScrollDimensions(); const scrollWidth = scrollDimensions.scrollWidth; - const scrollHeight = this._getTotalHeight(width, height, scrollWidth); - - this.scrollable.setScrollDimensions({ - width: width, - height: height, - scrollHeight: scrollHeight - }); + this._scrollable.setScrollDimensions(new EditorScrollDimensions( + width, + scrollDimensions.contentWidth, + height, + this._getContentHeight(width, height, scrollWidth) + )); } else { this._updateHeight(); } @@ -108,7 +247,7 @@ export class ViewLayout extends Disposable implements IViewLayout { return scrollbar.horizontalScrollbarSize; } - private _getTotalHeight(width: number, height: number, scrollWidth: number): number { + private _getContentHeight(width: number, height: number, scrollWidth: number): number { const options = this._configuration.options; let result = this._linesLayout.getLinesTotalHeight(); @@ -118,25 +257,27 @@ export class ViewLayout extends Disposable implements IViewLayout { result += this._getHorizontalScrollbarHeight(width, scrollWidth); } - return Math.max(height, result); + return result; } private _updateHeight(): void { - const scrollDimensions = this.scrollable.getScrollDimensions(); + const scrollDimensions = this._scrollable.getScrollDimensions(); const width = scrollDimensions.width; const height = scrollDimensions.height; const scrollWidth = scrollDimensions.scrollWidth; - const scrollHeight = this._getTotalHeight(width, height, scrollWidth); - this.scrollable.setScrollDimensions({ - scrollHeight: scrollHeight - }); + this._scrollable.setScrollDimensions(new EditorScrollDimensions( + width, + scrollDimensions.contentWidth, + height, + this._getContentHeight(width, height, scrollWidth) + )); } // ---- Layouting logic public getCurrentViewport(): Viewport { - const scrollDimensions = this.scrollable.getScrollDimensions(); - const currentScrollPosition = this.scrollable.getCurrentScrollPosition(); + const scrollDimensions = this._scrollable.getScrollDimensions(); + const currentScrollPosition = this._scrollable.getCurrentScrollPosition(); return new Viewport( currentScrollPosition.scrollTop, currentScrollPosition.scrollLeft, @@ -146,8 +287,8 @@ export class ViewLayout extends Disposable implements IViewLayout { } public getFutureViewport(): Viewport { - const scrollDimensions = this.scrollable.getScrollDimensions(); - const currentScrollPosition = this.scrollable.getFutureScrollPosition(); + const scrollDimensions = this._scrollable.getScrollDimensions(); + const currentScrollPosition = this._scrollable.getFutureScrollPosition(); return new Viewport( currentScrollPosition.scrollTop, currentScrollPosition.scrollLeft, @@ -156,23 +297,27 @@ export class ViewLayout extends Disposable implements IViewLayout { ); } - private _computeScrollWidth(maxLineWidth: number, viewportWidth: number): number { + private _computeContentWidth(maxLineWidth: number): number { const options = this._configuration.options; const wrappingInfo = options.get(EditorOption.wrappingInfo); let isViewportWrapping = wrappingInfo.isViewportWrapping; if (!isViewportWrapping) { const extraHorizontalSpace = options.get(EditorOption.scrollBeyondLastColumn) * options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; const whitespaceMinWidth = this._linesLayout.getWhitespaceMinWidth(); - return Math.max(maxLineWidth + extraHorizontalSpace, viewportWidth, whitespaceMinWidth); + return Math.max(maxLineWidth + extraHorizontalSpace, whitespaceMinWidth); } - return Math.max(maxLineWidth, viewportWidth); + return maxLineWidth; } public onMaxLineWidthChanged(maxLineWidth: number): void { - let newScrollWidth = this._computeScrollWidth(maxLineWidth, this.getCurrentViewport().width); - this.scrollable.setScrollDimensions({ - scrollWidth: newScrollWidth - }); + const scrollDimensions = this._scrollable.getScrollDimensions(); + // const newScrollWidth = ; + this._scrollable.setScrollDimensions(new EditorScrollDimensions( + scrollDimensions.width, + this._computeContentWidth(maxLineWidth), + scrollDimensions.height, + scrollDimensions.contentHeight + )); // The height might depend on the fact that there is a horizontal scrollbar or not this._updateHeight(); @@ -181,7 +326,7 @@ export class ViewLayout extends Disposable implements IViewLayout { // ---- view state public saveState(): { scrollTop: number; scrollTopWithoutViewZones: number; scrollLeft: number; } { - const currentScrollPosition = this.scrollable.getFutureScrollPosition(); + const currentScrollPosition = this._scrollable.getFutureScrollPosition(); let scrollTop = currentScrollPosition.scrollTop; let firstLineNumberInViewport = this._linesLayout.getLineNumberAtOrAfterVerticalOffset(scrollTop); let whitespaceAboveFirstLine = this._linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(firstLineNumberInViewport); @@ -215,7 +360,7 @@ export class ViewLayout extends Disposable implements IViewLayout { } public getLinesViewportDataAtScrollTop(scrollTop: number): IPartialViewLinesViewportData { // do some minimal validations on scrollTop - const scrollDimensions = this.scrollable.getScrollDimensions(); + const scrollDimensions = this._scrollable.getScrollDimensions(); if (scrollTop + scrollDimensions.height > scrollDimensions.scrollHeight) { scrollTop = scrollDimensions.scrollHeight - scrollDimensions.height; } @@ -234,40 +379,47 @@ export class ViewLayout extends Disposable implements IViewLayout { // ---- IScrollingProvider - + public getContentWidth(): number { + const scrollDimensions = this._scrollable.getScrollDimensions(); + return scrollDimensions.contentWidth; + } public getScrollWidth(): number { - const scrollDimensions = this.scrollable.getScrollDimensions(); + const scrollDimensions = this._scrollable.getScrollDimensions(); return scrollDimensions.scrollWidth; } + public getContentHeight(): number { + const scrollDimensions = this._scrollable.getScrollDimensions(); + return scrollDimensions.contentHeight; + } public getScrollHeight(): number { - const scrollDimensions = this.scrollable.getScrollDimensions(); + const scrollDimensions = this._scrollable.getScrollDimensions(); return scrollDimensions.scrollHeight; } public getCurrentScrollLeft(): number { - const currentScrollPosition = this.scrollable.getCurrentScrollPosition(); + const currentScrollPosition = this._scrollable.getCurrentScrollPosition(); return currentScrollPosition.scrollLeft; } public getCurrentScrollTop(): number { - const currentScrollPosition = this.scrollable.getCurrentScrollPosition(); + const currentScrollPosition = this._scrollable.getCurrentScrollPosition(); return currentScrollPosition.scrollTop; } - public validateScrollPosition(scrollPosition: editorCommon.INewScrollPosition): IScrollPosition { - return this.scrollable.validateScrollPosition(scrollPosition); + public validateScrollPosition(scrollPosition: INewScrollPosition): IScrollPosition { + return this._scrollable.validateScrollPosition(scrollPosition); } - public setScrollPositionNow(position: editorCommon.INewScrollPosition): void { - this.scrollable.setScrollPositionNow(position); + public setScrollPositionNow(position: INewScrollPosition): void { + this._scrollable.setScrollPositionNow(position); } - public setScrollPositionSmooth(position: editorCommon.INewScrollPosition): void { - this.scrollable.setScrollPositionSmooth(position); + public setScrollPositionSmooth(position: INewScrollPosition): void { + this._scrollable.setScrollPositionSmooth(position); } public deltaScrollNow(deltaScrollLeft: number, deltaScrollTop: number): void { - const currentScrollPosition = this.scrollable.getCurrentScrollPosition(); - this.scrollable.setScrollPositionNow({ + const currentScrollPosition = this._scrollable.getCurrentScrollPosition(); + this._scrollable.setScrollPositionNow({ scrollLeft: currentScrollPosition.scrollLeft + deltaScrollLeft, scrollTop: currentScrollPosition.scrollTop + deltaScrollTop }); diff --git a/src/vs/editor/common/viewLayout/viewLineRenderer.ts b/src/vs/editor/common/viewLayout/viewLineRenderer.ts index d7aeddd3d9..1addc868a1 100644 --- a/src/vs/editor/common/viewLayout/viewLineRenderer.ts +++ b/src/vs/editor/common/viewLayout/viewLineRenderer.ts @@ -66,6 +66,7 @@ export class RenderLineInput { public readonly lineTokens: IViewLineTokens; public readonly lineDecorations: LineDecoration[]; public readonly tabSize: number; + public readonly startVisibleColumn: number; public readonly spaceWidth: number; public readonly stopRenderingLineAfter: number; public readonly renderWhitespace: RenderWhitespace; @@ -89,6 +90,7 @@ export class RenderLineInput { lineTokens: IViewLineTokens, lineDecorations: LineDecoration[], tabSize: number, + startVisibleColumn: number, spaceWidth: number, stopRenderingLineAfter: number, renderWhitespace: 'none' | 'boundary' | 'selection' | 'all', @@ -106,6 +108,7 @@ export class RenderLineInput { this.lineTokens = lineTokens; this.lineDecorations = lineDecorations; this.tabSize = tabSize; + this.startVisibleColumn = startVisibleColumn; this.spaceWidth = spaceWidth; this.stopRenderingLineAfter = stopRenderingLineAfter; this.renderWhitespace = ( @@ -154,6 +157,7 @@ export class RenderLineInput { && this.containsRTL === other.containsRTL && this.fauxIndentLength === other.fauxIndentLength && this.tabSize === other.tabSize + && this.startVisibleColumn === other.startVisibleColumn && this.spaceWidth === other.spaceWidth && this.stopRenderingLineAfter === other.stopRenderingLineAfter && this.renderWhitespace === other.renderWhitespace @@ -314,21 +318,24 @@ export function renderViewLine(input: RenderLineInput, sb: IStringBuilder): Rend if (input.lineDecorations.length > 0) { // This line is empty, but it contains inline decorations - let classNames: string[] = []; + const beforeClassNames: string[] = []; + const afterClassNames: string[] = []; for (let i = 0, len = input.lineDecorations.length; i < len; i++) { const lineDecoration = input.lineDecorations[i]; if (lineDecoration.type === InlineDecorationType.Before) { - classNames.push(input.lineDecorations[i].className); + beforeClassNames.push(input.lineDecorations[i].className); containsForeignElements |= ForeignElementType.Before; } if (lineDecoration.type === InlineDecorationType.After) { - classNames.push(input.lineDecorations[i].className); + afterClassNames.push(input.lineDecorations[i].className); containsForeignElements |= ForeignElementType.After; } } if (containsForeignElements !== ForeignElementType.None) { - content = ``; + const beforeSpan = (beforeClassNames.length > 0 ? `` : ``); + const afterSpan = (afterClassNames.length > 0 ? `` : ``); + content = `${beforeSpan}${afterSpan}`; } } @@ -368,7 +375,9 @@ class ResolvedRenderLineInput { public readonly isOverflowing: boolean, public readonly parts: LinePart[], public readonly containsForeignElements: ForeignElementType, + public readonly fauxIndentLength: number, public readonly tabSize: number, + public readonly startVisibleColumn: number, public readonly containsRTL: boolean, public readonly spaceWidth: number, public readonly renderWhitespace: RenderWhitespace, @@ -395,7 +404,7 @@ function resolveRenderLineInput(input: RenderLineInput): ResolvedRenderLineInput let tokens = transformAndRemoveOverflowing(input.lineTokens, input.fauxIndentLength, len); if (input.renderWhitespace === RenderWhitespace.All || input.renderWhitespace === RenderWhitespace.Boundary || (input.renderWhitespace === RenderWhitespace.Selection && !!input.selectionsOnLine)) { - tokens = _applyRenderWhitespace(lineContent, len, input.continuesWithWrappedLine, tokens, input.fauxIndentLength, input.tabSize, useMonospaceOptimizations, input.selectionsOnLine, input.renderWhitespace === RenderWhitespace.Boundary); + tokens = _applyRenderWhitespace(lineContent, len, input.continuesWithWrappedLine, tokens, input.fauxIndentLength, input.tabSize, input.startVisibleColumn, useMonospaceOptimizations, input.selectionsOnLine, input.renderWhitespace === RenderWhitespace.Boundary); } let containsForeignElements = ForeignElementType.None; if (input.lineDecorations.length > 0) { @@ -425,7 +434,9 @@ function resolveRenderLineInput(input: RenderLineInput): ResolvedRenderLineInput isOverflowing, tokens, containsForeignElements, + input.fauxIndentLength, input.tabSize, + input.startVisibleColumn, input.containsRTL, input.spaceWidth, input.renderWhitespace, @@ -537,7 +548,7 @@ function splitLargeTokens(lineContent: string, tokens: LinePart[], onlyAtSpaces: * Moreover, a token is created for every visual indent because on some fonts the glyphs used for rendering whitespace (→ or ·) do not have the same width as  . * The rendering phase will generate `style="width:..."` for these tokens. */ -function _applyRenderWhitespace(lineContent: string, len: number, continuesWithWrappedLine: boolean, tokens: LinePart[], fauxIndentLength: number, tabSize: number, useMonospaceOptimizations: boolean, selections: LineRange[] | null, onlyBoundary: boolean): LinePart[] { +function _applyRenderWhitespace(lineContent: string, len: number, continuesWithWrappedLine: boolean, tokens: LinePart[], fauxIndentLength: number, tabSize: number, startVisibleColumn: number, useMonospaceOptimizations: boolean, selections: LineRange[] | null, onlyBoundary: boolean): LinePart[] { let result: LinePart[] = [], resultLen = 0; let tokenIndex = 0; @@ -555,21 +566,10 @@ function _applyRenderWhitespace(lineContent: string, len: number, continuesWithW lastNonWhitespaceIndex = strings.lastNonWhitespaceIndex(lineContent); } - let tmpIndent = 0; - for (let charIndex = 0; charIndex < fauxIndentLength; charIndex++) { - const chCode = lineContent.charCodeAt(charIndex); - if (chCode === CharCode.Tab) { - tmpIndent = tabSize; - } else if (strings.isFullWidthCharacter(chCode)) { - tmpIndent += 2; - } else { - tmpIndent++; - } - } - tmpIndent = tmpIndent % tabSize; let wasInWhitespace = false; let currentSelectionIndex = 0; let currentSelection = selections && selections[currentSelectionIndex]; + let tmpIndent = startVisibleColumn % tabSize; for (let charIndex = fauxIndentLength; charIndex < len; charIndex++) { const chCode = lineContent.charCodeAt(charIndex); @@ -729,7 +729,9 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render const len = input.len; const isOverflowing = input.isOverflowing; const parts = input.parts; + const fauxIndentLength = input.fauxIndentLength; const tabSize = input.tabSize; + const startVisibleColumn = input.startVisibleColumn; const containsRTL = input.containsRTL; const spaceWidth = input.spaceWidth; const renderWhitespace = input.renderWhitespace; @@ -738,7 +740,7 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render const characterMapping = new CharacterMapping(len + 1, parts.length); let charIndex = 0; - let tabsCharDelta = 0; + let visibleColumn = startVisibleColumn; let charOffsetInPart = 0; let prevPartContentCnt = 0; @@ -764,18 +766,14 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render let partContentCnt = 0; { let _charIndex = charIndex; - let _tabsCharDelta = tabsCharDelta; + let _visibleColumn = visibleColumn; for (; _charIndex < partEndIndex; _charIndex++) { const charCode = lineContent.charCodeAt(_charIndex); - - if (charCode === CharCode.Tab) { - let insertSpacesCount = tabSize - (_charIndex + _tabsCharDelta) % tabSize; - _tabsCharDelta += insertSpacesCount - 1; - partContentCnt += insertSpacesCount; - } else { - // must be CharCode.Space - partContentCnt++; + const charWidth = (charCode === CharCode.Tab ? (tabSize - (_visibleColumn % tabSize)) : 1) | 0; + partContentCnt += charWidth; + if (_charIndex >= fauxIndentLength) { + _visibleColumn += charWidth; } } } @@ -793,29 +791,30 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render for (; charIndex < partEndIndex; charIndex++) { characterMapping.setPartData(charIndex, partIndex, charOffsetInPart, partAbsoluteOffset); const charCode = lineContent.charCodeAt(charIndex); + let charWidth: number; if (charCode === CharCode.Tab) { - let insertSpacesCount = tabSize - (charIndex + tabsCharDelta) % tabSize; - tabsCharDelta += insertSpacesCount - 1; - charOffsetInPart += insertSpacesCount - 1; - if (insertSpacesCount > 0) { - if (!canUseHalfwidthRightwardsArrow || insertSpacesCount > 1) { - sb.write1(0x2192); // RIGHTWARDS ARROW - } else { - sb.write1(0xFFEB); // HALFWIDTH RIGHTWARDS ARROW - } - insertSpacesCount--; + charWidth = (tabSize - (visibleColumn % tabSize)) | 0; + + if (!canUseHalfwidthRightwardsArrow || charWidth > 1) { + sb.write1(0x2192); // RIGHTWARDS ARROW + } else { + sb.write1(0xFFEB); // HALFWIDTH RIGHTWARDS ARROW } - while (insertSpacesCount > 0) { + for (let space = 2; space <= charWidth; space++) { sb.write1(0xA0); //   - insertSpacesCount--; } - } else { - // must be CharCode.Space + + } else { // must be CharCode.Space + charWidth = 1; + sb.write1(0xB7); // · } - charOffsetInPart++; + charOffsetInPart += charWidth; + if (charIndex >= fauxIndentLength) { + visibleColumn += charWidth; + } } prevPartContentCnt = partContentCnt; @@ -833,63 +832,59 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render characterMapping.setPartData(charIndex, partIndex, charOffsetInPart, partAbsoluteOffset); const charCode = lineContent.charCodeAt(charIndex); + let producedCharacters = 1; + let charWidth = 1; + switch (charCode) { case CharCode.Tab: - let insertSpacesCount = tabSize - (charIndex + tabsCharDelta) % tabSize; - tabsCharDelta += insertSpacesCount - 1; - charOffsetInPart += insertSpacesCount - 1; - while (insertSpacesCount > 0) { + producedCharacters = (tabSize - (visibleColumn % tabSize)); + charWidth = producedCharacters; + for (let space = 1; space <= producedCharacters; space++) { sb.write1(0xA0); //   - partContentCnt++; - insertSpacesCount--; } break; case CharCode.Space: sb.write1(0xA0); //   - partContentCnt++; break; case CharCode.LessThan: sb.appendASCIIString('<'); - partContentCnt++; break; case CharCode.GreaterThan: sb.appendASCIIString('>'); - partContentCnt++; break; case CharCode.Ampersand: sb.appendASCIIString('&'); - partContentCnt++; break; case CharCode.Null: sb.appendASCIIString('�'); - partContentCnt++; break; case CharCode.UTF8_BOM: case CharCode.LINE_SEPARATOR_2028: sb.write1(0xFFFD); - partContentCnt++; break; default: if (strings.isFullWidthCharacter(charCode)) { - tabsCharDelta++; + charWidth++; } if (renderControlCharacters && charCode < 32) { sb.write1(9216 + charCode); - partContentCnt++; } else { sb.write1(charCode); - partContentCnt++; } } - charOffsetInPart++; + charOffsetInPart += producedCharacters; + partContentCnt += producedCharacters; + if (charIndex >= fauxIndentLength) { + visibleColumn += charWidth; + } } prevPartContentCnt = partContentCnt; diff --git a/src/vs/editor/common/viewModel/characterHardWrappingLineMapper.ts b/src/vs/editor/common/viewModel/characterHardWrappingLineMapper.ts deleted file mode 100644 index df96d613ab..0000000000 --- a/src/vs/editor/common/viewModel/characterHardWrappingLineMapper.ts +++ /dev/null @@ -1,293 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { CharCode } from 'vs/base/common/charCode'; -import * as strings from 'vs/base/common/strings'; -import { WrappingIndent } from 'vs/editor/common/config/editorOptions'; -import { CharacterClassifier } from 'vs/editor/common/core/characterClassifier'; -import { toUint32Array } from 'vs/base/common/uint'; -import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; -import { ILineMapperFactory, ILineMapping, OutputPosition } from 'vs/editor/common/viewModel/splitLinesCollection'; - -const enum CharacterClass { - NONE = 0, - BREAK_BEFORE = 1, - BREAK_AFTER = 2, - BREAK_OBTRUSIVE = 3, - BREAK_IDEOGRAPHIC = 4 // for Han and Kana. -} - -class WrappingCharacterClassifier extends CharacterClassifier { - - constructor(BREAK_BEFORE: string, BREAK_AFTER: string, BREAK_OBTRUSIVE: string) { - super(CharacterClass.NONE); - - for (let i = 0; i < BREAK_BEFORE.length; i++) { - this.set(BREAK_BEFORE.charCodeAt(i), CharacterClass.BREAK_BEFORE); - } - - for (let i = 0; i < BREAK_AFTER.length; i++) { - this.set(BREAK_AFTER.charCodeAt(i), CharacterClass.BREAK_AFTER); - } - - for (let i = 0; i < BREAK_OBTRUSIVE.length; i++) { - this.set(BREAK_OBTRUSIVE.charCodeAt(i), CharacterClass.BREAK_OBTRUSIVE); - } - } - - public get(charCode: number): CharacterClass { - // Initialize CharacterClass.BREAK_IDEOGRAPHIC for these Unicode ranges: - // 1. CJK Unified Ideographs (0x4E00 -- 0x9FFF) - // 2. CJK Unified Ideographs Extension A (0x3400 -- 0x4DBF) - // 3. Hiragana and Katakana (0x3040 -- 0x30FF) - if ( - (charCode >= 0x3040 && charCode <= 0x30FF) - || (charCode >= 0x3400 && charCode <= 0x4DBF) - || (charCode >= 0x4E00 && charCode <= 0x9FFF) - ) { - return CharacterClass.BREAK_IDEOGRAPHIC; - } - - return super.get(charCode); - } -} - -export class CharacterHardWrappingLineMapperFactory implements ILineMapperFactory { - - private readonly classifier: WrappingCharacterClassifier; - - constructor(breakBeforeChars: string, breakAfterChars: string, breakObtrusiveChars: string) { - this.classifier = new WrappingCharacterClassifier(breakBeforeChars, breakAfterChars, breakObtrusiveChars); - } - - // TODO@Alex -> duplicated in lineCommentCommand - private static nextVisibleColumn(currentVisibleColumn: number, tabSize: number, isTab: boolean, columnSize: number): number { - currentVisibleColumn = +currentVisibleColumn; //@perf - tabSize = +tabSize; //@perf - columnSize = +columnSize; //@perf - - if (isTab) { - return currentVisibleColumn + (tabSize - (currentVisibleColumn % tabSize)); - } - return currentVisibleColumn + columnSize; - } - - public createLineMapping(lineText: string, tabSize: number, breakingColumn: number, columnsForFullWidthChar: number, hardWrappingIndent: WrappingIndent): ILineMapping | null { - if (breakingColumn === -1) { - return null; - } - - tabSize = +tabSize; //@perf - breakingColumn = +breakingColumn; //@perf - columnsForFullWidthChar = +columnsForFullWidthChar; //@perf - hardWrappingIndent = +hardWrappingIndent; //@perf - - let wrappedTextIndentVisibleColumn = 0; - let wrappedTextIndent = ''; - - let firstNonWhitespaceIndex = -1; - if (hardWrappingIndent !== WrappingIndent.None) { - firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineText); - if (firstNonWhitespaceIndex !== -1) { - // Track existing indent - wrappedTextIndent = lineText.substring(0, firstNonWhitespaceIndex); - for (let i = 0; i < firstNonWhitespaceIndex; i++) { - wrappedTextIndentVisibleColumn = CharacterHardWrappingLineMapperFactory.nextVisibleColumn(wrappedTextIndentVisibleColumn, tabSize, lineText.charCodeAt(i) === CharCode.Tab, 1); - } - - // Increase indent of continuation lines, if desired - let numberOfAdditionalTabs = 0; - if (hardWrappingIndent === WrappingIndent.Indent) { - numberOfAdditionalTabs = 1; - } else if (hardWrappingIndent === WrappingIndent.DeepIndent) { - numberOfAdditionalTabs = 2; - } - for (let i = 0; i < numberOfAdditionalTabs; i++) { - wrappedTextIndent += '\t'; - wrappedTextIndentVisibleColumn = CharacterHardWrappingLineMapperFactory.nextVisibleColumn(wrappedTextIndentVisibleColumn, tabSize, true, 1); - } - - // Force sticking to beginning of line if no character would fit except for the indentation - if (wrappedTextIndentVisibleColumn + columnsForFullWidthChar > breakingColumn) { - wrappedTextIndent = ''; - wrappedTextIndentVisibleColumn = 0; - } - } - } - - let classifier = this.classifier; - let lastBreakingOffset = 0; // Last 0-based offset in the lineText at which a break happened - let breakingLengths: number[] = []; // The length of each broken-up line text - let breakingLengthsIndex: number = 0; // The count of breaks already done - let visibleColumn = 0; // Visible column since the beginning of the current line - let niceBreakOffset = -1; // Last index of a character that indicates a break should happen before it (more desirable) - let niceBreakVisibleColumn = 0; // visible column if a break were to be later introduced before `niceBreakOffset` - let obtrusiveBreakOffset = -1; // Last index of a character that indicates a break should happen before it (less desirable) - let obtrusiveBreakVisibleColumn = 0; // visible column if a break were to be later introduced before `obtrusiveBreakOffset` - let len = lineText.length; - - for (let i = 0; i < len; i++) { - // At this point, there is a certainty that the character before `i` fits on the current line, - // but the character at `i` might not fit - - let charCode = lineText.charCodeAt(i); - let charCodeIsTab = (charCode === CharCode.Tab); - let charCodeClass = classifier.get(charCode); - - if (strings.isLowSurrogate(charCode)/* && i + 1 < len */) { - // A surrogate pair must always be considered as a single unit, so it is never to be broken - // => advance visibleColumn by 1 and advance to next char code... - visibleColumn = visibleColumn + 1; - continue; - } - - if (charCodeClass === CharacterClass.BREAK_BEFORE) { - // This is a character that indicates that a break should happen before it - // Since we are certain the character before `i` fits, there's no extra checking needed, - // just mark it as a nice breaking opportunity - niceBreakOffset = i; - niceBreakVisibleColumn = wrappedTextIndentVisibleColumn; - } - - // CJK breaking : before break - if (charCodeClass === CharacterClass.BREAK_IDEOGRAPHIC && i > 0) { - let prevCode = lineText.charCodeAt(i - 1); - let prevClass = classifier.get(prevCode); - if (prevClass !== CharacterClass.BREAK_BEFORE) { // Kinsoku Shori: Don't break after a leading character, like an open bracket - niceBreakOffset = i; - niceBreakVisibleColumn = wrappedTextIndentVisibleColumn; - } - } - - let charColumnSize = 1; - if (strings.isFullWidthCharacter(charCode)) { - charColumnSize = columnsForFullWidthChar; - } - - // Advance visibleColumn with character at `i` - visibleColumn = CharacterHardWrappingLineMapperFactory.nextVisibleColumn(visibleColumn, tabSize, charCodeIsTab, charColumnSize); - - if (visibleColumn > breakingColumn && i !== 0) { - // We need to break at least before character at `i`: - // - break before niceBreakLastOffset if it exists (and re-establish a correct visibleColumn by using niceBreakVisibleColumn + charAt(i)) - // - otherwise, break before obtrusiveBreakLastOffset if it exists (and re-establish a correct visibleColumn by using obtrusiveBreakVisibleColumn + charAt(i)) - // - otherwise, break before i (and re-establish a correct visibleColumn by charAt(i)) - - let breakBeforeOffset: number; - let restoreVisibleColumnFrom: number; - - if (niceBreakOffset !== -1 && niceBreakVisibleColumn <= breakingColumn) { - - // We will break before `niceBreakLastOffset` - breakBeforeOffset = niceBreakOffset; - restoreVisibleColumnFrom = niceBreakVisibleColumn; - - } else if (obtrusiveBreakOffset !== -1 && obtrusiveBreakVisibleColumn <= breakingColumn) { - - // We will break before `obtrusiveBreakLastOffset` - breakBeforeOffset = obtrusiveBreakOffset; - restoreVisibleColumnFrom = obtrusiveBreakVisibleColumn; - - } else { - - // We will break before `i` - breakBeforeOffset = i; - restoreVisibleColumnFrom = wrappedTextIndentVisibleColumn; - - } - - // Break before character at `breakBeforeOffset` - breakingLengths[breakingLengthsIndex++] = breakBeforeOffset - lastBreakingOffset; - lastBreakingOffset = breakBeforeOffset; - - // Re-establish visibleColumn by taking character at `i` into account - visibleColumn = CharacterHardWrappingLineMapperFactory.nextVisibleColumn(restoreVisibleColumnFrom, tabSize, charCodeIsTab, charColumnSize); - - // Reset markers - niceBreakOffset = -1; - niceBreakVisibleColumn = 0; - obtrusiveBreakOffset = -1; - obtrusiveBreakVisibleColumn = 0; - } - - // At this point, there is a certainty that the character at `i` fits on the current line - - if (niceBreakOffset !== -1) { - // Advance niceBreakVisibleColumn - niceBreakVisibleColumn = CharacterHardWrappingLineMapperFactory.nextVisibleColumn(niceBreakVisibleColumn, tabSize, charCodeIsTab, charColumnSize); - } - if (obtrusiveBreakOffset !== -1) { - // Advance obtrusiveBreakVisibleColumn - obtrusiveBreakVisibleColumn = CharacterHardWrappingLineMapperFactory.nextVisibleColumn(obtrusiveBreakVisibleColumn, tabSize, charCodeIsTab, charColumnSize); - } - - if (charCodeClass === CharacterClass.BREAK_AFTER && (hardWrappingIndent === WrappingIndent.None || i >= firstNonWhitespaceIndex)) { - // This is a character that indicates that a break should happen after it - niceBreakOffset = i + 1; - niceBreakVisibleColumn = wrappedTextIndentVisibleColumn; - } - - // CJK breaking : after break - if (charCodeClass === CharacterClass.BREAK_IDEOGRAPHIC && i < len - 1) { - let nextCode = lineText.charCodeAt(i + 1); - let nextClass = classifier.get(nextCode); - if (nextClass !== CharacterClass.BREAK_AFTER) { // Kinsoku Shori: Don't break before a trailing character, like a period - niceBreakOffset = i + 1; - niceBreakVisibleColumn = wrappedTextIndentVisibleColumn; - } - } - - if (charCodeClass === CharacterClass.BREAK_OBTRUSIVE) { - // This is an obtrusive character that indicates that a break should happen after it - obtrusiveBreakOffset = i + 1; - obtrusiveBreakVisibleColumn = wrappedTextIndentVisibleColumn; - } - } - - if (breakingLengthsIndex === 0) { - return null; - } - - // Add last segment - breakingLengths[breakingLengthsIndex++] = len - lastBreakingOffset; - - return new CharacterHardWrappingLineMapping( - new PrefixSumComputer(toUint32Array(breakingLengths)), - wrappedTextIndent - ); - } -} - -export class CharacterHardWrappingLineMapping implements ILineMapping { - - private readonly _prefixSums: PrefixSumComputer; - private readonly _wrappedLinesIndent: string; - - constructor(prefixSums: PrefixSumComputer, wrappedLinesIndent: string) { - this._prefixSums = prefixSums; - this._wrappedLinesIndent = wrappedLinesIndent; - } - - public getOutputLineCount(): number { - return this._prefixSums.getCount(); - } - - public getWrappedLinesIndent(): string { - return this._wrappedLinesIndent; - } - - public getInputOffsetOfOutputPosition(outputLineIndex: number, outputOffset: number): number { - if (outputLineIndex === 0) { - return outputOffset; - } else { - return this._prefixSums.getAccumulatedValue(outputLineIndex - 1) + outputOffset; - } - } - - public getOutputPositionOfInputOffset(inputOffset: number): OutputPosition { - let r = this._prefixSums.getIndexOf(inputOffset); - return new OutputPosition(r.index, r.remainder); - } -} diff --git a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts new file mode 100644 index 0000000000..d1d85739d1 --- /dev/null +++ b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts @@ -0,0 +1,472 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CharCode } from 'vs/base/common/charCode'; +import * as strings from 'vs/base/common/strings'; +import { WrappingIndent, IComputedEditorOptions, EditorOption } from 'vs/editor/common/config/editorOptions'; +import { CharacterClassifier } from 'vs/editor/common/core/characterClassifier'; +import { ILineBreaksComputerFactory, LineBreakData, ILineBreaksComputer } from 'vs/editor/common/viewModel/splitLinesCollection'; +import { FontInfo } from 'vs/editor/common/config/fontInfo'; + +const enum CharacterClass { + NONE = 0, + BREAK_BEFORE = 1, + BREAK_AFTER = 2, + BREAK_IDEOGRAPHIC = 3 // for Han and Kana. +} + +class WrappingCharacterClassifier extends CharacterClassifier { + + constructor(BREAK_BEFORE: string, BREAK_AFTER: string) { + super(CharacterClass.NONE); + + for (let i = 0; i < BREAK_BEFORE.length; i++) { + this.set(BREAK_BEFORE.charCodeAt(i), CharacterClass.BREAK_BEFORE); + } + + for (let i = 0; i < BREAK_AFTER.length; i++) { + this.set(BREAK_AFTER.charCodeAt(i), CharacterClass.BREAK_AFTER); + } + } + + public get(charCode: number): CharacterClass { + if (charCode >= 0 && charCode < 256) { + return this._asciiMap[charCode]; + } else { + // Initialize CharacterClass.BREAK_IDEOGRAPHIC for these Unicode ranges: + // 1. CJK Unified Ideographs (0x4E00 -- 0x9FFF) + // 2. CJK Unified Ideographs Extension A (0x3400 -- 0x4DBF) + // 3. Hiragana and Katakana (0x3040 -- 0x30FF) + if ( + (charCode >= 0x3040 && charCode <= 0x30FF) + || (charCode >= 0x3400 && charCode <= 0x4DBF) + || (charCode >= 0x4E00 && charCode <= 0x9FFF) + ) { + return CharacterClass.BREAK_IDEOGRAPHIC; + } + + return (this._map.get(charCode) || this._defaultValue); + } + } +} + +let arrPool1: number[] = []; +let arrPool2: number[] = []; + +export class MonospaceLineBreaksComputerFactory implements ILineBreaksComputerFactory { + + public static create(options: IComputedEditorOptions): MonospaceLineBreaksComputerFactory { + return new MonospaceLineBreaksComputerFactory( + options.get(EditorOption.wordWrapBreakBeforeCharacters), + options.get(EditorOption.wordWrapBreakAfterCharacters) + ); + } + + private readonly classifier: WrappingCharacterClassifier; + + constructor(breakBeforeChars: string, breakAfterChars: string) { + this.classifier = new WrappingCharacterClassifier(breakBeforeChars, breakAfterChars); + } + + public createLineBreaksComputer(fontInfo: FontInfo, tabSize: number, wrappingColumn: number, wrappingIndent: WrappingIndent): ILineBreaksComputer { + tabSize = tabSize | 0; //@perf + wrappingColumn = +wrappingColumn; //@perf + + let requests: string[] = []; + let previousBreakingData: (LineBreakData | null)[] = []; + return { + addRequest: (lineText: string, previousLineBreakData: LineBreakData | null) => { + requests.push(lineText); + previousBreakingData.push(previousLineBreakData); + }, + finalize: () => { + const columnsForFullWidthChar = fontInfo.typicalFullwidthCharacterWidth / fontInfo.typicalHalfwidthCharacterWidth; //@perf + let result: (LineBreakData | null)[] = []; + for (let i = 0, len = requests.length; i < len; i++) { + const previousLineBreakData = previousBreakingData[i]; + if (previousLineBreakData) { + result[i] = createLineBreaksFromPreviousLineBreaks(this.classifier, previousLineBreakData, requests[i], tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent); + } else { + result[i] = createLineBreaks(this.classifier, requests[i], tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent); + } + } + arrPool1.length = 0; + arrPool2.length = 0; + return result; + } + }; + } +} + +function createLineBreaksFromPreviousLineBreaks(classifier: WrappingCharacterClassifier, previousBreakingData: LineBreakData, lineText: string, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent): LineBreakData | null { + if (firstLineBreakColumn === -1) { + return null; + } + + const len = lineText.length; + if (len <= 1) { + return null; + } + + const prevBreakingOffsets = previousBreakingData.breakOffsets; + const prevBreakingOffsetsVisibleColumn = previousBreakingData.breakOffsetsVisibleColumn; + + const wrappedTextIndentLength = computeWrappedTextIndentLength(lineText, tabSize, firstLineBreakColumn, columnsForFullWidthChar, wrappingIndent); + const wrappedLineBreakColumn = firstLineBreakColumn - wrappedTextIndentLength; + + let breakingOffsets: number[] = arrPool1; + let breakingOffsetsVisibleColumn: number[] = arrPool2; + let breakingOffsetsCount: number = 0; + + let breakingColumn = firstLineBreakColumn; + const prevLen = prevBreakingOffsets.length; + let prevIndex = 0; + + if (prevIndex >= 0) { + let bestDistance = Math.abs(prevBreakingOffsetsVisibleColumn[prevIndex] - breakingColumn); + while (prevIndex + 1 < prevLen) { + const distance = Math.abs(prevBreakingOffsetsVisibleColumn[prevIndex + 1] - breakingColumn); + if (distance >= bestDistance) { + break; + } + bestDistance = distance; + prevIndex++; + } + } + + while (prevIndex < prevLen) { + // Allow for prevIndex to be -1 (for the case where we hit a tab when walking backwards from the first break) + const prevBreakOffset = prevIndex < 0 ? 0 : prevBreakingOffsets[prevIndex]; + const prevBreakoffsetVisibleColumn = prevIndex < 0 ? 0 : prevBreakingOffsetsVisibleColumn[prevIndex]; + + let breakOffset = 0; + let breakOffsetVisibleColumn = 0; + + let forcedBreakOffset = 0; + let forcedBreakOffsetVisibleColumn = 0; + + // initially, we search as much as possible to the right (if it fits) + if (prevBreakoffsetVisibleColumn <= breakingColumn) { + let visibleColumn = prevBreakoffsetVisibleColumn; + let prevCharCode = lineText.charCodeAt(prevBreakOffset - 1); + let prevCharCodeClass = classifier.get(prevCharCode); + let entireLineFits = true; + for (let i = prevBreakOffset; i < len; i++) { + const charStartOffset = i; + const charCode = lineText.charCodeAt(i); + let charCodeClass: number; + let charWidth: number; + + if (strings.isHighSurrogate(charCode)) { + // A surrogate pair must always be considered as a single unit, so it is never to be broken + i++; + charCodeClass = CharacterClass.NONE; + charWidth = 2; + } else { + charCodeClass = classifier.get(charCode); + charWidth = computeCharWidth(charCode, visibleColumn, tabSize, columnsForFullWidthChar); + } + + if (canBreak(prevCharCode, prevCharCodeClass, charCode, charCodeClass)) { + breakOffset = charStartOffset; + breakOffsetVisibleColumn = visibleColumn; + } + + visibleColumn += charWidth; + + // check if adding character at `i` will go over the breaking column + if (visibleColumn > breakingColumn) { + // We need to break at least before character at `i`: + forcedBreakOffset = charStartOffset; + forcedBreakOffsetVisibleColumn = visibleColumn - charWidth; + + if (visibleColumn - breakOffsetVisibleColumn > wrappedLineBreakColumn) { + // Cannot break at `breakOffset` => reset it if it was set + breakOffset = 0; + } + + entireLineFits = false; + break; + } + + prevCharCode = charCode; + prevCharCodeClass = charCodeClass; + } + + if (entireLineFits) { + // there is no more need to break => stop the outer loop! + if (breakingOffsetsCount > 0) { + // Add last segment + breakingOffsets[breakingOffsetsCount] = prevBreakingOffsets[prevBreakingOffsets.length - 1]; + breakingOffsetsVisibleColumn[breakingOffsetsCount] = prevBreakingOffsetsVisibleColumn[prevBreakingOffsets.length - 1]; + breakingOffsetsCount++; + } + break; + } + } + + if (breakOffset === 0) { + // must search left + let visibleColumn = prevBreakoffsetVisibleColumn; + let charCode = lineText.charCodeAt(prevBreakOffset); + let charCodeClass = classifier.get(charCode); + let hitATabCharacter = false; + for (let i = prevBreakOffset - 1; i >= 0; i--) { + const charStartOffset = i + 1; + const prevCharCode = lineText.charCodeAt(i); + + if (prevCharCode === CharCode.Tab) { + // cannot determine the width of a tab when going backwards, so we must go forwards + hitATabCharacter = true; + break; + } + + let prevCharCodeClass: number; + let prevCharWidth: number; + + if (strings.isLowSurrogate(prevCharCode)) { + // A surrogate pair must always be considered as a single unit, so it is never to be broken + i--; + prevCharCodeClass = CharacterClass.NONE; + prevCharWidth = 2; + } else { + prevCharCodeClass = classifier.get(prevCharCode); + prevCharWidth = (strings.isFullWidthCharacter(prevCharCode) ? columnsForFullWidthChar : 1); + } + + if (visibleColumn <= breakingColumn) { + if (forcedBreakOffset === 0) { + forcedBreakOffset = charStartOffset; + forcedBreakOffsetVisibleColumn = visibleColumn; + } + + if (visibleColumn <= breakingColumn - wrappedLineBreakColumn) { + // went too far! + break; + } + + if (canBreak(prevCharCode, prevCharCodeClass, charCode, charCodeClass)) { + breakOffset = charStartOffset; + breakOffsetVisibleColumn = visibleColumn; + break; + } + } + + visibleColumn -= prevCharWidth; + charCode = prevCharCode; + charCodeClass = prevCharCodeClass; + } + + if (breakOffset !== 0) { + const remainingWidthOfNextLine = wrappedLineBreakColumn - (forcedBreakOffsetVisibleColumn - breakOffsetVisibleColumn); + if (remainingWidthOfNextLine <= tabSize) { + const charCodeAtForcedBreakOffset = lineText.charCodeAt(forcedBreakOffset); + let charWidth: number; + if (strings.isHighSurrogate(charCodeAtForcedBreakOffset)) { + // A surrogate pair must always be considered as a single unit, so it is never to be broken + charWidth = 2; + } else { + charWidth = computeCharWidth(charCodeAtForcedBreakOffset, forcedBreakOffsetVisibleColumn, tabSize, columnsForFullWidthChar); + } + if (remainingWidthOfNextLine - charWidth < 0) { + // it is not worth it to break at breakOffset, it just introduces an extra needless line! + breakOffset = 0; + } + } + } + + if (hitATabCharacter) { + // cannot determine the width of a tab when going backwards, so we must go forwards from the previous break + prevIndex--; + continue; + } + } + + if (breakOffset === 0) { + // Could not find a good breaking point + breakOffset = forcedBreakOffset; + breakOffsetVisibleColumn = forcedBreakOffsetVisibleColumn; + } + + breakingOffsets[breakingOffsetsCount] = breakOffset; + breakingOffsetsVisibleColumn[breakingOffsetsCount] = breakOffsetVisibleColumn; + breakingOffsetsCount++; + breakingColumn = breakOffsetVisibleColumn + wrappedLineBreakColumn; + + while (prevIndex < 0 || (prevIndex < prevLen && prevBreakingOffsetsVisibleColumn[prevIndex] < breakOffsetVisibleColumn)) { + prevIndex++; + } + + let bestDistance = Math.abs(prevBreakingOffsetsVisibleColumn[prevIndex] - breakingColumn); + while (prevIndex + 1 < prevLen) { + const distance = Math.abs(prevBreakingOffsetsVisibleColumn[prevIndex + 1] - breakingColumn); + if (distance >= bestDistance) { + break; + } + bestDistance = distance; + prevIndex++; + } + } + + if (breakingOffsetsCount === 0) { + return null; + } + + // Doing here some object reuse which ends up helping a huge deal with GC pauses! + breakingOffsets.length = breakingOffsetsCount; + breakingOffsetsVisibleColumn.length = breakingOffsetsCount; + arrPool1 = previousBreakingData.breakOffsets; + arrPool2 = previousBreakingData.breakOffsetsVisibleColumn; + previousBreakingData.breakOffsets = breakingOffsets; + previousBreakingData.breakOffsetsVisibleColumn = breakingOffsetsVisibleColumn; + previousBreakingData.wrappedTextIndentLength = wrappedTextIndentLength; + return previousBreakingData; +} + +function createLineBreaks(classifier: WrappingCharacterClassifier, lineText: string, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent): LineBreakData | null { + if (firstLineBreakColumn === -1) { + return null; + } + + const len = lineText.length; + if (len <= 1) { + return null; + } + + const wrappedTextIndentLength = computeWrappedTextIndentLength(lineText, tabSize, firstLineBreakColumn, columnsForFullWidthChar, wrappingIndent); + const wrappedLineBreakColumn = firstLineBreakColumn - wrappedTextIndentLength; + + let breakingOffsets: number[] = []; + let breakingOffsetsVisibleColumn: number[] = []; + let breakingOffsetsCount: number = 0; + let breakOffset = 0; + let breakOffsetVisibleColumn = 0; + + let breakingColumn = firstLineBreakColumn; + let prevCharCode = lineText.charCodeAt(0); + let prevCharCodeClass = classifier.get(prevCharCode); + let visibleColumn = computeCharWidth(prevCharCode, 0, tabSize, columnsForFullWidthChar); + + let startOffset = 1; + if (strings.isHighSurrogate(prevCharCode)) { + // A surrogate pair must always be considered as a single unit, so it is never to be broken + visibleColumn += 1; + prevCharCode = lineText.charCodeAt(1); + prevCharCodeClass = classifier.get(prevCharCode); + startOffset++; + } + + for (let i = startOffset; i < len; i++) { + const charStartOffset = i; + const charCode = lineText.charCodeAt(i); + let charCodeClass: number; + let charWidth: number; + + if (strings.isHighSurrogate(charCode)) { + // A surrogate pair must always be considered as a single unit, so it is never to be broken + i++; + charCodeClass = CharacterClass.NONE; + charWidth = 2; + } else { + charCodeClass = classifier.get(charCode); + charWidth = computeCharWidth(charCode, visibleColumn, tabSize, columnsForFullWidthChar); + } + + if (canBreak(prevCharCode, prevCharCodeClass, charCode, charCodeClass)) { + breakOffset = charStartOffset; + breakOffsetVisibleColumn = visibleColumn; + } + + visibleColumn += charWidth; + + // check if adding character at `i` will go over the breaking column + if (visibleColumn > breakingColumn) { + // We need to break at least before character at `i`: + + if (breakOffset === 0 || visibleColumn - breakOffsetVisibleColumn > wrappedLineBreakColumn) { + // Cannot break at `breakOffset`, must break at `i` + breakOffset = charStartOffset; + breakOffsetVisibleColumn = visibleColumn - charWidth; + } + + breakingOffsets[breakingOffsetsCount] = breakOffset; + breakingOffsetsVisibleColumn[breakingOffsetsCount] = breakOffsetVisibleColumn; + breakingOffsetsCount++; + breakingColumn = breakOffsetVisibleColumn + wrappedLineBreakColumn; + breakOffset = 0; + } + + prevCharCode = charCode; + prevCharCodeClass = charCodeClass; + } + + if (breakingOffsetsCount === 0) { + return null; + } + + // Add last segment + breakingOffsets[breakingOffsetsCount] = len; + breakingOffsetsVisibleColumn[breakingOffsetsCount] = visibleColumn; + + return new LineBreakData(breakingOffsets, breakingOffsetsVisibleColumn, wrappedTextIndentLength); +} + +function computeCharWidth(charCode: number, visibleColumn: number, tabSize: number, columnsForFullWidthChar: number): number { + if (charCode === CharCode.Tab) { + return (tabSize - (visibleColumn % tabSize)); + } + if (strings.isFullWidthCharacter(charCode)) { + return columnsForFullWidthChar; + } + return 1; +} + +function tabCharacterWidth(visibleColumn: number, tabSize: number): number { + return (tabSize - (visibleColumn % tabSize)); +} + +/** + * Kinsoku Shori : Don't break after a leading character, like an open bracket + * Kinsoku Shori : Don't break before a trailing character, like a period + */ +function canBreak(prevCharCode: number, prevCharCodeClass: CharacterClass, charCode: number, charCodeClass: CharacterClass): boolean { + return ( + charCode !== CharCode.Space + && ( + (prevCharCodeClass === CharacterClass.BREAK_AFTER) + || (prevCharCodeClass === CharacterClass.BREAK_IDEOGRAPHIC && charCodeClass !== CharacterClass.BREAK_AFTER) + || (charCodeClass === CharacterClass.BREAK_BEFORE) + || (charCodeClass === CharacterClass.BREAK_IDEOGRAPHIC && prevCharCodeClass !== CharacterClass.BREAK_BEFORE) + ) + ); +} + +function computeWrappedTextIndentLength(lineText: string, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent): number { + let wrappedTextIndentLength = 0; + if (wrappingIndent !== WrappingIndent.None) { + const firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineText); + if (firstNonWhitespaceIndex !== -1) { + // Track existing indent + + for (let i = 0; i < firstNonWhitespaceIndex; i++) { + const charWidth = (lineText.charCodeAt(i) === CharCode.Tab ? tabCharacterWidth(wrappedTextIndentLength, tabSize) : 1); + wrappedTextIndentLength += charWidth; + } + + // Increase indent of continuation lines, if desired + const numberOfAdditionalTabs = (wrappingIndent === WrappingIndent.DeepIndent ? 2 : wrappingIndent === WrappingIndent.Indent ? 1 : 0); + for (let i = 0; i < numberOfAdditionalTabs; i++) { + const charWidth = tabCharacterWidth(wrappedTextIndentLength, tabSize); + wrappedTextIndentLength += charWidth; + } + + // Force sticking to beginning of line if no character would fit except for the indentation + if (wrappedTextIndentLength + columnsForFullWidthChar > firstLineBreakColumn) { + wrappedTextIndentLength = 0; + } + } + } + return wrappedTextIndentLength; +} diff --git a/src/vs/editor/common/viewModel/prefixSumComputer.ts b/src/vs/editor/common/viewModel/prefixSumComputer.ts index a0e9eec728..2904c9e447 100644 --- a/src/vs/editor/common/viewModel/prefixSumComputer.ts +++ b/src/vs/editor/common/viewModel/prefixSumComputer.ts @@ -187,73 +187,3 @@ export class PrefixSumComputer { return new PrefixSumIndexOfResult(mid, accumulatedValue - midStart); } } - -export class PrefixSumComputerWithCache { - - private readonly _actual: PrefixSumComputer; - private _cacheAccumulatedValueStart: number = 0; - private _cache: PrefixSumIndexOfResult[] | null = null; - - constructor(values: Uint32Array) { - this._actual = new PrefixSumComputer(values); - this._bustCache(); - } - - private _bustCache(): void { - this._cacheAccumulatedValueStart = 0; - this._cache = null; - } - - public insertValues(insertIndex: number, insertValues: Uint32Array): void { - if (this._actual.insertValues(insertIndex, insertValues)) { - this._bustCache(); - } - } - - public changeValue(index: number, value: number): void { - if (this._actual.changeValue(index, value)) { - this._bustCache(); - } - } - - public removeValues(startIndex: number, cnt: number): void { - if (this._actual.removeValues(startIndex, cnt)) { - this._bustCache(); - } - } - - public getTotalValue(): number { - return this._actual.getTotalValue(); - } - - public getAccumulatedValue(index: number): number { - return this._actual.getAccumulatedValue(index); - } - - public getIndexOf(accumulatedValue: number): PrefixSumIndexOfResult { - accumulatedValue = Math.floor(accumulatedValue); //@perf - - if (this._cache !== null) { - let cacheIndex = accumulatedValue - this._cacheAccumulatedValueStart; - if (cacheIndex >= 0 && cacheIndex < this._cache.length) { - // Cache hit! - return this._cache[cacheIndex]; - } - } - - // Cache miss! - return this._actual.getIndexOf(accumulatedValue); - } - - /** - * Gives a hint that a lot of requests are about to come in for these accumulated values. - */ - public warmUpCache(accumulatedValueStart: number, accumulatedValueEnd: number): void { - let newCache: PrefixSumIndexOfResult[] = []; - for (let accumulatedValue = accumulatedValueStart; accumulatedValue <= accumulatedValueEnd; accumulatedValue++) { - newCache[accumulatedValue - accumulatedValueStart] = this.getIndexOf(accumulatedValue); - } - this._cache = newCache; - this._cacheAccumulatedValueStart = accumulatedValueStart; - } -} diff --git a/src/vs/editor/common/viewModel/splitLinesCollection.ts b/src/vs/editor/common/viewModel/splitLinesCollection.ts index 88fd115463..5b109f8dc8 100644 --- a/src/vs/editor/common/viewModel/splitLinesCollection.ts +++ b/src/vs/editor/common/viewModel/splitLinesCollection.ts @@ -3,6 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as arrays from 'vs/base/common/arrays'; import { WrappingIndent } from 'vs/editor/common/config/editorOptions'; import { LineTokens } from 'vs/editor/common/core/lineTokens'; import { Position } from 'vs/editor/common/core/position'; @@ -10,13 +11,13 @@ import { IRange, Range } from 'vs/editor/common/core/range'; import { EndOfLinePreference, IActiveIndentGuideInfo, IModelDecoration, IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; import { ModelDecorationOptions, ModelDecorationOverviewRulerOptions } from 'vs/editor/common/model/textModel'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; -import { PrefixSumComputerWithCache } from 'vs/editor/common/viewModel/prefixSumComputer'; +import { PrefixSumIndexOfResult } from 'vs/editor/common/viewModel/prefixSumComputer'; import { ICoordinatesConverter, IOverviewRulerDecorations, ViewLineData } from 'vs/editor/common/viewModel/viewModel'; import { ITheme } from 'vs/platform/theme/common/themeService'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { FontInfo } from 'vs/editor/common/config/fontInfo'; export class OutputPosition { - _outputPositionBrand: void; outputLineIndex: number; outputOffset: number; @@ -26,15 +27,56 @@ export class OutputPosition { } } -export interface ILineMapping { - getOutputLineCount(): number; - getWrappedLinesIndent(): string; - getInputOffsetOfOutputPosition(outputLineIndex: number, outputOffset: number): number; - getOutputPositionOfInputOffset(inputOffset: number): OutputPosition; +export class LineBreakData { + constructor( + public breakOffsets: number[], + public breakOffsetsVisibleColumn: number[], + public wrappedTextIndentLength: number + ) { } + + public static getInputOffsetOfOutputPosition(breakOffsets: number[], outputLineIndex: number, outputOffset: number): number { + if (outputLineIndex === 0) { + return outputOffset; + } else { + return breakOffsets[outputLineIndex - 1] + outputOffset; + } + } + + public static getOutputPositionOfInputOffset(breakOffsets: number[], inputOffset: number): OutputPosition { + let low = 0; + let high = breakOffsets.length - 1; + let mid = 0; + let midStart = 0; + + while (low <= high) { + mid = low + ((high - low) / 2) | 0; + + const midStop = breakOffsets[mid]; + midStart = mid > 0 ? breakOffsets[mid - 1] : 0; + + if (inputOffset < midStart) { + high = mid - 1; + } else if (inputOffset >= midStop) { + low = mid + 1; + } else { + break; + } + } + + return new OutputPosition(mid, inputOffset - midStart); + } } -export interface ILineMapperFactory { - createLineMapping(lineText: string, tabSize: number, wrappingColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent): ILineMapping | null; +export interface ILineBreaksComputer { + /** + * Pass in `previousLineBreakData` if the only difference is in breaking columns!!! + */ + addRequest(lineText: string, previousLineBreakData: LineBreakData | null): void; + finalize(): (LineBreakData | null)[]; +} + +export interface ILineBreaksComputerFactory { + createLineBreaksComputer(fontInfo: FontInfo, tabSize: number, wrappingColumn: number, wrappingIndent: WrappingIndent): ILineBreaksComputer; } export interface ISimpleModel { @@ -50,6 +92,7 @@ export interface ISplitLine { isVisible(): boolean; setVisible(isVisible: boolean): ISplitLine; + getLineBreakData(): LineBreakData | null; getViewLineCount(): number; getViewLineContent(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): string; getViewLineLength(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): number; @@ -66,19 +109,19 @@ export interface ISplitLine { export interface IViewModelLinesCollection extends IDisposable { createCoordinatesConverter(): ICoordinatesConverter; - setWrappingSettings(wrappingIndent: WrappingIndent, wrappingColumn: number, columnsForFullWidthChar: number): boolean; + setWrappingSettings(fontInfo: FontInfo, wrappingAlgorithm: 'monospace' | 'dom', wrappingColumn: number, wrappingIndent: WrappingIndent): boolean; setTabSize(newTabSize: number): boolean; getHiddenAreas(): Range[]; setHiddenAreas(_ranges: Range[]): boolean; + createLineBreaksComputer(): ILineBreaksComputer; onModelFlushed(): void; onModelLinesDeleted(versionId: number, fromLineNumber: number, toLineNumber: number): viewEvents.ViewLinesDeletedEvent | null; - onModelLinesInserted(versionId: number, fromLineNumber: number, toLineNumber: number, text: string[]): viewEvents.ViewLinesInsertedEvent | null; - onModelLineChanged(versionId: number, lineNumber: number, newText: string): [boolean, viewEvents.ViewLinesChangedEvent | null, viewEvents.ViewLinesInsertedEvent | null, viewEvents.ViewLinesDeletedEvent | null]; + onModelLinesInserted(versionId: number, fromLineNumber: number, toLineNumber: number, lineBreaks: (LineBreakData | null)[]): viewEvents.ViewLinesInsertedEvent | null; + onModelLineChanged(versionId: number, lineNumber: number, lineBreakData: LineBreakData | null): [boolean, viewEvents.ViewLinesChangedEvent | null, viewEvents.ViewLinesInsertedEvent | null, viewEvents.ViewLinesDeletedEvent | null]; acceptVersionId(versionId: number): void; getViewLineCount(): number; - warmUpLookupCache(viewStartLineNumber: number, viewEndLineNumber: number): void; getActiveIndentGuide(viewLineNumber: number, minLineNumber: number, maxLineNumber: number): IActiveIndentGuideInfo; getViewLinesIndentGuides(viewStartLineNumber: number, viewEndLineNumber: number): number[]; getViewLineContent(viewLineNumber: number): string; @@ -107,9 +150,7 @@ export class CoordinatesConverter implements ICoordinatesConverter { } public convertViewRangeToModelRange(viewRange: Range): Range { - let start = this._lines.convertViewPositionToModelPosition(viewRange.startLineNumber, viewRange.startColumn); - let end = this._lines.convertViewPositionToModelPosition(viewRange.endLineNumber, viewRange.endColumn); - return new Range(start.lineNumber, start.column, end.lineNumber, end.column); + return this._lines.convertViewRangeToModelRange(viewRange); } public validateViewPosition(viewPosition: Position, expectedModelPosition: Position): Position { @@ -117,9 +158,7 @@ export class CoordinatesConverter implements ICoordinatesConverter { } public validateViewRange(viewRange: Range, expectedModelRange: Range): Range { - const validViewStart = this._lines.validateViewPosition(viewRange.startLineNumber, viewRange.startColumn, expectedModelRange.getStartPosition()); - const validViewEnd = this._lines.validateViewPosition(viewRange.endLineNumber, viewRange.endColumn, expectedModelRange.getEndPosition()); - return new Range(validViewStart.lineNumber, validViewStart.column, validViewEnd.lineNumber, validViewEnd.column); + return this._lines.validateViewRange(viewRange, expectedModelRange); } // Model -> View conversion and related methods @@ -135,7 +174,6 @@ export class CoordinatesConverter implements ICoordinatesConverter { public modelPositionIsVisible(modelPosition: Position): boolean { return this._lines.modelPositionIsVisible(modelPosition.lineNumber, modelPosition.column); } - } const enum IndentGuideRepeatOption { @@ -144,33 +182,129 @@ const enum IndentGuideRepeatOption { BlockAll = 2 } +class LineNumberMapper { + + private _counts: number[]; + private _isValid: boolean; + private _validEndIndex: number; + + private _modelToView: number[]; + private _viewToModel: number[]; + + constructor(viewLineCounts: number[]) { + this._counts = viewLineCounts; + this._isValid = false; + this._validEndIndex = -1; + this._modelToView = []; + this._viewToModel = []; + } + + private _invalidate(index: number): void { + this._isValid = false; + this._validEndIndex = Math.min(this._validEndIndex, index - 1); + } + + private _ensureValid(): void { + if (this._isValid) { + return; + } + + for (let i = this._validEndIndex + 1, len = this._counts.length; i < len; i++) { + const viewLineCount = this._counts[i]; + const viewLinesAbove = (i > 0 ? this._modelToView[i - 1] : 0); + + this._modelToView[i] = viewLinesAbove + viewLineCount; + for (let j = 0; j < viewLineCount; j++) { + this._viewToModel[viewLinesAbove + j] = i; + } + } + + // trim things + this._modelToView.length = this._counts.length; + this._viewToModel.length = this._modelToView[this._modelToView.length - 1]; + + // mark as valid + this._isValid = true; + this._validEndIndex = this._counts.length - 1; + } + + public changeValue(index: number, value: number): void { + if (this._counts[index] === value) { + // no change + return; + } + this._counts[index] = value; + this._invalidate(index); + } + + public removeValues(start: number, deleteCount: number): void { + this._counts.splice(start, deleteCount); + this._invalidate(start); + } + + public insertValues(insertIndex: number, insertArr: number[]): void { + this._counts = arrays.arrayInsert(this._counts, insertIndex, insertArr); + this._invalidate(insertIndex); + } + + public getTotalValue(): number { + this._ensureValid(); + return this._viewToModel.length; + } + + public getAccumulatedValue(index: number): number { + this._ensureValid(); + return this._modelToView[index]; + } + + public getIndexOf(accumulatedValue: number): PrefixSumIndexOfResult { + this._ensureValid(); + const modelLineIndex = this._viewToModel[accumulatedValue]; + const viewLinesAbove = (modelLineIndex > 0 ? this._modelToView[modelLineIndex - 1] : 0); + return new PrefixSumIndexOfResult(modelLineIndex, accumulatedValue - viewLinesAbove); + } +} + export class SplitLinesCollection implements IViewModelLinesCollection { private readonly model: ITextModel; private _validModelVersionId: number; - private wrappingColumn: number; - private columnsForFullWidthChar: number; - private wrappingIndent: WrappingIndent; + private readonly _domLineBreaksComputerFactory: ILineBreaksComputerFactory; + private readonly _monospaceLineBreaksComputerFactory: ILineBreaksComputerFactory; + + private fontInfo: FontInfo; private tabSize: number; + private wrappingColumn: number; + private wrappingIndent: WrappingIndent; + private wrappingAlgorithm: 'monospace' | 'dom'; private lines!: ISplitLine[]; - private prefixSumComputer!: PrefixSumComputerWithCache; - - private readonly linePositionMapperFactory: ILineMapperFactory; + private prefixSumComputer!: LineNumberMapper; private hiddenAreasIds!: string[]; - constructor(model: ITextModel, linePositionMapperFactory: ILineMapperFactory, tabSize: number, wrappingColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent) { + constructor( + model: ITextModel, + domLineBreaksComputerFactory: ILineBreaksComputerFactory, + monospaceLineBreaksComputerFactory: ILineBreaksComputerFactory, + fontInfo: FontInfo, + tabSize: number, + wrappingAlgorithm: 'monospace' | 'dom', + wrappingColumn: number, + wrappingIndent: WrappingIndent, + ) { this.model = model; this._validModelVersionId = -1; + this._domLineBreaksComputerFactory = domLineBreaksComputerFactory; + this._monospaceLineBreaksComputerFactory = monospaceLineBreaksComputerFactory; + this.fontInfo = fontInfo; this.tabSize = tabSize; + this.wrappingAlgorithm = wrappingAlgorithm; this.wrappingColumn = wrappingColumn; - this.columnsForFullWidthChar = columnsForFullWidthChar; this.wrappingIndent = wrappingIndent; - this.linePositionMapperFactory = linePositionMapperFactory; - this._constructLines(true); + this._constructLines(/*resetHiddenAreas*/true, null); } public dispose(): void { @@ -181,19 +315,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection { return new CoordinatesConverter(this); } - private _ensureValidState(): void { - let modelVersion = this.model.getVersionId(); - if (modelVersion !== this._validModelVersionId) { - // This is pretty bad, it means we lost track of the model... - throw new Error(`ViewModel is out of sync with Model!`); - } - if (this.lines.length !== this.model.getLineCount()) { - // This is pretty bad, it means we lost track of the model... - this._constructLines(false); - } - } - - private _constructLines(resetHiddenAreas: boolean): void { + private _constructLines(resetHiddenAreas: boolean, previousLineBreaks: ((LineBreakData | null)[]) | null): void { this.lines = []; if (resetHiddenAreas) { @@ -201,8 +323,14 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } let linesContent = this.model.getLinesContent(); - let lineCount = linesContent.length; - let values = new Uint32Array(lineCount); + const lineCount = linesContent.length; + const lineBreaksComputer = this.createLineBreaksComputer(); + for (let i = 0; i < lineCount; i++) { + lineBreaksComputer.addRequest(linesContent[i], previousLineBreaks ? previousLineBreaks[i] : null); + } + const linesBreaks = lineBreaksComputer.finalize(); + + let values: number[] = []; let hiddenAreas = this.hiddenAreasIds.map((areaId) => this.model.getDecorationRange(areaId)!).sort(Range.compareRangesUsingStarts); let hiddenAreaStart = 1, hiddenAreaEnd = 0; @@ -220,14 +348,14 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } let isInHiddenArea = (lineNumber >= hiddenAreaStart && lineNumber <= hiddenAreaEnd); - let line = createSplitLine(this.linePositionMapperFactory, linesContent[i], this.tabSize, this.wrappingColumn, this.columnsForFullWidthChar, this.wrappingIndent, !isInHiddenArea); + let line = createSplitLine(linesBreaks[i], !isInHiddenArea); values[i] = line.getViewLineCount(); this.lines[i] = line; } this._validModelVersionId = this.model.getVersionId(); - this.prefixSumComputer = new PrefixSumComputerWithCache(values); + this.prefixSumComputer = new LineNumberMapper(values); } public getHiddenAreas(): Range[] { @@ -351,27 +479,51 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } this.tabSize = newTabSize; - this._constructLines(false); + this._constructLines(/*resetHiddenAreas*/false, null); return true; } - public setWrappingSettings(wrappingIndent: WrappingIndent, wrappingColumn: number, columnsForFullWidthChar: number): boolean { - if (this.wrappingIndent === wrappingIndent && this.wrappingColumn === wrappingColumn && this.columnsForFullWidthChar === columnsForFullWidthChar) { + public setWrappingSettings(fontInfo: FontInfo, wrappingAlgorithm: 'monospace' | 'dom', wrappingColumn: number, wrappingIndent: WrappingIndent): boolean { + const equalFontInfo = this.fontInfo.equals(fontInfo); + const equalWrappingAlgorithm = (this.wrappingAlgorithm === wrappingAlgorithm); + const equalWrappingColumn = (this.wrappingColumn === wrappingColumn); + const equalWrappingIndent = (this.wrappingIndent === wrappingIndent); + if (equalFontInfo && equalWrappingAlgorithm && equalWrappingColumn && equalWrappingIndent) { return false; } - this.wrappingIndent = wrappingIndent; - this.wrappingColumn = wrappingColumn; - this.columnsForFullWidthChar = columnsForFullWidthChar; + const onlyWrappingColumnChanged = (equalFontInfo && equalWrappingAlgorithm && !equalWrappingColumn && equalWrappingIndent); - this._constructLines(false); + this.fontInfo = fontInfo; + this.wrappingAlgorithm = wrappingAlgorithm; + this.wrappingColumn = wrappingColumn; + this.wrappingIndent = wrappingIndent; + + let previousLineBreaks: ((LineBreakData | null)[]) | null = null; + if (onlyWrappingColumnChanged) { + previousLineBreaks = []; + for (let i = 0, len = this.lines.length; i < len; i++) { + previousLineBreaks[i] = this.lines[i].getLineBreakData(); + } + } + + this._constructLines(/*resetHiddenAreas*/false, previousLineBreaks); return true; } + public createLineBreaksComputer(): ILineBreaksComputer { + const lineBreaksComputerFactory = ( + this.wrappingAlgorithm === 'dom' + ? this._domLineBreaksComputerFactory + : this._monospaceLineBreaksComputerFactory + ); + return lineBreaksComputerFactory.createLineBreaksComputer(this.fontInfo, this.tabSize, this.wrappingColumn, this.wrappingIndent); + } + public onModelFlushed(): void { - this._constructLines(true); + this._constructLines(/*resetHiddenAreas*/true, null); } public onModelLinesDeleted(versionId: number, fromLineNumber: number, toLineNumber: number): viewEvents.ViewLinesDeletedEvent | null { @@ -390,7 +542,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection { return new viewEvents.ViewLinesDeletedEvent(outputFromLineNumber, outputToLineNumber); } - public onModelLinesInserted(versionId: number, fromLineNumber: number, _toLineNumber: number, text: string[]): viewEvents.ViewLinesInsertedEvent | null { + public onModelLinesInserted(versionId: number, fromLineNumber: number, _toLineNumber: number, lineBreaks: (LineBreakData | null)[]): viewEvents.ViewLinesInsertedEvent | null { if (versionId <= this._validModelVersionId) { // Here we check for versionId in case the lines were reconstructed in the meantime. // We don't want to apply stale change events on top of a newer read model state. @@ -411,10 +563,10 @@ export class SplitLinesCollection implements IViewModelLinesCollection { let totalOutputLineCount = 0; let insertLines: ISplitLine[] = []; - let insertPrefixSumValues = new Uint32Array(text.length); + let insertPrefixSumValues: number[] = []; - for (let i = 0, len = text.length; i < len; i++) { - let line = createSplitLine(this.linePositionMapperFactory, text[i], this.tabSize, this.wrappingColumn, this.columnsForFullWidthChar, this.wrappingIndent, !isInHiddenArea); + for (let i = 0, len = lineBreaks.length; i < len; i++) { + let line = createSplitLine(lineBreaks[i], !isInHiddenArea); insertLines.push(line); let outputLineCount = line.getViewLineCount(); @@ -430,7 +582,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection { return new viewEvents.ViewLinesInsertedEvent(outputFromLineNumber, outputFromLineNumber + totalOutputLineCount - 1); } - public onModelLineChanged(versionId: number, lineNumber: number, newText: string): [boolean, viewEvents.ViewLinesChangedEvent | null, viewEvents.ViewLinesInsertedEvent | null, viewEvents.ViewLinesDeletedEvent | null] { + public onModelLineChanged(versionId: number, lineNumber: number, lineBreakData: LineBreakData | null): [boolean, viewEvents.ViewLinesChangedEvent | null, viewEvents.ViewLinesInsertedEvent | null, viewEvents.ViewLinesDeletedEvent | null] { if (versionId <= this._validModelVersionId) { // Here we check for versionId in case the lines were reconstructed in the meantime. // We don't want to apply stale change events on top of a newer read model state. @@ -441,7 +593,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection { let oldOutputLineCount = this.lines[lineIndex].getViewLineCount(); let isVisible = this.lines[lineIndex].isVisible(); - let line = createSplitLine(this.linePositionMapperFactory, newText, this.tabSize, this.wrappingColumn, this.columnsForFullWidthChar, this.wrappingIndent, isVisible); + let line = createSplitLine(lineBreakData, isVisible); this.lines[lineIndex] = line; let newOutputLineCount = this.lines[lineIndex].getViewLineCount(); @@ -488,7 +640,6 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } public getViewLineCount(): number { - this._ensureValidState(); return this.prefixSumComputer.getTotalValue(); } @@ -496,22 +647,14 @@ export class SplitLinesCollection implements IViewModelLinesCollection { if (viewLineNumber < 1) { return 1; } - let viewLineCount = this.getViewLineCount(); + const viewLineCount = this.getViewLineCount(); if (viewLineNumber > viewLineCount) { return viewLineCount; } - return viewLineNumber; - } - - /** - * Gives a hint that a lot of requests are about to come in for these line numbers. - */ - public warmUpLookupCache(viewStartLineNumber: number, viewEndLineNumber: number): void { - this.prefixSumComputer.warmUpCache(viewStartLineNumber - 1, viewEndLineNumber - 1); + return viewLineNumber | 0; } public getActiveIndentGuide(viewLineNumber: number, minLineNumber: number, maxLineNumber: number): IActiveIndentGuideInfo { - this._ensureValidState(); viewLineNumber = this._toValidViewLineNumber(viewLineNumber); minLineNumber = this._toValidViewLineNumber(minLineNumber); maxLineNumber = this._toValidViewLineNumber(maxLineNumber); @@ -531,7 +674,6 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } public getViewLinesIndentGuides(viewStartLineNumber: number, viewEndLineNumber: number): number[] { - this._ensureValidState(); viewStartLineNumber = this._toValidViewLineNumber(viewStartLineNumber); viewEndLineNumber = this._toValidViewLineNumber(viewEndLineNumber); @@ -602,7 +744,6 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } public getViewLineContent(viewLineNumber: number): string { - this._ensureValidState(); viewLineNumber = this._toValidViewLineNumber(viewLineNumber); let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); let lineIndex = r.index; @@ -612,7 +753,6 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } public getViewLineLength(viewLineNumber: number): number { - this._ensureValidState(); viewLineNumber = this._toValidViewLineNumber(viewLineNumber); let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); let lineIndex = r.index; @@ -622,7 +762,6 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } public getViewLineMinColumn(viewLineNumber: number): number { - this._ensureValidState(); viewLineNumber = this._toValidViewLineNumber(viewLineNumber); let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); let lineIndex = r.index; @@ -632,7 +771,6 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } public getViewLineMaxColumn(viewLineNumber: number): number { - this._ensureValidState(); viewLineNumber = this._toValidViewLineNumber(viewLineNumber); let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); let lineIndex = r.index; @@ -642,7 +780,6 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } public getViewLineData(viewLineNumber: number): ViewLineData { - this._ensureValidState(); viewLineNumber = this._toValidViewLineNumber(viewLineNumber); let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); let lineIndex = r.index; @@ -652,7 +789,6 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } public getViewLinesData(viewStartLineNumber: number, viewEndLineNumber: number, needed: boolean[]): ViewLineData[] { - this._ensureValidState(); viewStartLineNumber = this._toValidViewLineNumber(viewStartLineNumber); viewEndLineNumber = this._toValidViewLineNumber(viewEndLineNumber); @@ -691,7 +827,6 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } public validateViewPosition(viewLineNumber: number, viewColumn: number, expectedModelPosition: Position): Position { - this._ensureValidState(); viewLineNumber = this._toValidViewLineNumber(viewLineNumber); let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); @@ -719,8 +854,13 @@ export class SplitLinesCollection implements IViewModelLinesCollection { return this.convertModelPositionToViewPosition(expectedModelPosition.lineNumber, expectedModelPosition.column); } + public validateViewRange(viewRange: Range, expectedModelRange: Range): Range { + const validViewStart = this.validateViewPosition(viewRange.startLineNumber, viewRange.startColumn, expectedModelRange.getStartPosition()); + const validViewEnd = this.validateViewPosition(viewRange.endLineNumber, viewRange.endColumn, expectedModelRange.getEndPosition()); + return new Range(validViewStart.lineNumber, validViewStart.column, validViewEnd.lineNumber, validViewEnd.column); + } + public convertViewPositionToModelPosition(viewLineNumber: number, viewColumn: number): Position { - this._ensureValidState(); viewLineNumber = this._toValidViewLineNumber(viewLineNumber); let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); @@ -732,8 +872,13 @@ export class SplitLinesCollection implements IViewModelLinesCollection { return this.model.validatePosition(new Position(lineIndex + 1, inputColumn)); } + public convertViewRangeToModelRange(viewRange: Range): Range { + const start = this.convertViewPositionToModelPosition(viewRange.startLineNumber, viewRange.startColumn); + const end = this.convertViewPositionToModelPosition(viewRange.endLineNumber, viewRange.endColumn); + return new Range(start.lineNumber, start.column, end.lineNumber, end.column); + } + public convertModelPositionToViewPosition(_modelLineNumber: number, _modelColumn: number): Position { - this._ensureValidState(); const validPosition = this.model.validatePosition(new Position(_modelLineNumber, _modelColumn)); const inputLineNumber = validPosition.lineNumber; @@ -898,6 +1043,10 @@ class VisibleIdentitySplitLine implements ISplitLine { return InvisibleIdentitySplitLine.INSTANCE; } + public getLineBreakData(): LineBreakData | null { + return null; + } + public getViewLineCount(): number { return 1; } @@ -926,6 +1075,7 @@ class VisibleIdentitySplitLine implements ISplitLine { false, 1, lineContent.length + 1, + 0, lineTokens.inflate() ); } @@ -968,6 +1118,10 @@ class InvisibleIdentitySplitLine implements ISplitLine { return VisibleIdentitySplitLine.INSTANCE; } + public getLineBreakData(): LineBreakData | null { + return null; + } + public getViewLineCount(): number { return 0; } @@ -1011,18 +1165,11 @@ class InvisibleIdentitySplitLine implements ISplitLine { export class SplitLine implements ISplitLine { - private readonly positionMapper: ILineMapping; - private readonly outputLineCount: number; - - private readonly wrappedIndent: string; - private readonly wrappedIndentLength: number; + private readonly _lineBreakData: LineBreakData; private _isVisible: boolean; - constructor(positionMapper: ILineMapping, isVisible: boolean) { - this.positionMapper = positionMapper; - this.wrappedIndent = this.positionMapper.getWrappedLinesIndent(); - this.wrappedIndentLength = this.wrappedIndent.length; - this.outputLineCount = this.positionMapper.getOutputLineCount(); + constructor(lineBreakData: LineBreakData, isVisible: boolean) { + this._lineBreakData = lineBreakData; this._isVisible = isVisible; } @@ -1035,22 +1182,26 @@ export class SplitLine implements ISplitLine { return this; } + public getLineBreakData(): LineBreakData | null { + return this._lineBreakData; + } + public getViewLineCount(): number { if (!this._isVisible) { return 0; } - return this.outputLineCount; + return this._lineBreakData.breakOffsets.length; } private getInputStartOffsetOfOutputLineIndex(outputLineIndex: number): number { - return this.positionMapper.getInputOffsetOfOutputPosition(outputLineIndex, 0); + return LineBreakData.getInputOffsetOfOutputPosition(this._lineBreakData.breakOffsets, outputLineIndex, 0); } private getInputEndOffsetOfOutputLineIndex(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): number { - if (outputLineIndex + 1 === this.outputLineCount) { + if (outputLineIndex + 1 === this._lineBreakData.breakOffsets.length) { return model.getLineMaxColumn(modelLineNumber) - 1; } - return this.positionMapper.getInputOffsetOfOutputPosition(outputLineIndex + 1, 0); + return LineBreakData.getInputOffsetOfOutputPosition(this._lineBreakData.breakOffsets, outputLineIndex + 1, 0); } public getViewLineContent(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): string { @@ -1067,7 +1218,7 @@ export class SplitLine implements ISplitLine { }); if (outputLineIndex > 0) { - r = this.wrappedIndent + r; + r = spaces(this._lineBreakData.wrappedTextIndentLength) + r; } return r; @@ -1082,7 +1233,7 @@ export class SplitLine implements ISplitLine { let r = endOffset - startOffset; if (outputLineIndex > 0) { - r = this.wrappedIndent.length + r; + r = this._lineBreakData.wrappedTextIndentLength + r; } return r; @@ -1093,7 +1244,7 @@ export class SplitLine implements ISplitLine { throw new Error('Not supported'); } if (outputLineIndex > 0) { - return this.wrappedIndentLength + 1; + return this._lineBreakData.wrappedTextIndentLength + 1; } return 1; } @@ -1121,25 +1272,28 @@ export class SplitLine implements ISplitLine { }); if (outputLineIndex > 0) { - lineContent = this.wrappedIndent + lineContent; + lineContent = spaces(this._lineBreakData.wrappedTextIndentLength) + lineContent; } - let minColumn = (outputLineIndex > 0 ? this.wrappedIndentLength + 1 : 1); + let minColumn = (outputLineIndex > 0 ? this._lineBreakData.wrappedTextIndentLength + 1 : 1); let maxColumn = lineContent.length + 1; let continuesWithWrappedLine = (outputLineIndex + 1 < this.getViewLineCount()); let deltaStartIndex = 0; if (outputLineIndex > 0) { - deltaStartIndex = this.wrappedIndentLength; + deltaStartIndex = this._lineBreakData.wrappedTextIndentLength; } let lineTokens = model.getLineTokens(modelLineNumber); + const startVisibleColumn = (outputLineIndex === 0 ? 0 : this._lineBreakData.breakOffsetsVisibleColumn[outputLineIndex - 1]); + return new ViewLineData( lineContent, continuesWithWrappedLine, minColumn, maxColumn, + startVisibleColumn, lineTokens.sliceAndInflate(startOffset, endOffset, deltaStartIndex) ); } @@ -1165,25 +1319,25 @@ export class SplitLine implements ISplitLine { } let adjustedColumn = outputColumn - 1; if (outputLineIndex > 0) { - if (adjustedColumn < this.wrappedIndentLength) { + if (adjustedColumn < this._lineBreakData.wrappedTextIndentLength) { adjustedColumn = 0; } else { - adjustedColumn -= this.wrappedIndentLength; + adjustedColumn -= this._lineBreakData.wrappedTextIndentLength; } } - return this.positionMapper.getInputOffsetOfOutputPosition(outputLineIndex, adjustedColumn) + 1; + return LineBreakData.getInputOffsetOfOutputPosition(this._lineBreakData.breakOffsets, outputLineIndex, adjustedColumn) + 1; } public getViewPositionOfModelPosition(deltaLineNumber: number, inputColumn: number): Position { if (!this._isVisible) { throw new Error('Not supported'); } - let r = this.positionMapper.getOutputPositionOfInputOffset(inputColumn - 1); + let r = LineBreakData.getOutputPositionOfInputOffset(this._lineBreakData.breakOffsets, inputColumn - 1); let outputLineIndex = r.outputLineIndex; let outputColumn = r.outputOffset + 1; if (outputLineIndex > 0) { - outputColumn += this.wrappedIndentLength; + outputColumn += this._lineBreakData.wrappedTextIndentLength; } // console.log('in -> out ' + deltaLineNumber + ',' + inputColumn + ' ===> ' + (deltaLineNumber+outputLineIndex) + ',' + outputColumn); @@ -1194,21 +1348,33 @@ export class SplitLine implements ISplitLine { if (!this._isVisible) { throw new Error('Not supported'); } - const r = this.positionMapper.getOutputPositionOfInputOffset(inputColumn - 1); + const r = LineBreakData.getOutputPositionOfInputOffset(this._lineBreakData.breakOffsets, inputColumn - 1); return (deltaLineNumber + r.outputLineIndex); } } -function createSplitLine(linePositionMapperFactory: ILineMapperFactory, text: string, tabSize: number, wrappingColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent, isVisible: boolean): ISplitLine { - let positionMapper = linePositionMapperFactory.createLineMapping(text, tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent); - if (positionMapper === null) { +let _spaces: string[] = ['']; +function spaces(count: number): string { + if (count >= _spaces.length) { + for (let i = 1; i <= count; i++) { + _spaces[i] = _makeSpaces(i); + } + } + return _spaces[count]; +} +function _makeSpaces(count: number): string { + return new Array(count + 1).join(' '); +} + +function createSplitLine(lineBreakData: LineBreakData | null, isVisible: boolean): ISplitLine { + if (lineBreakData === null) { // No mapping needed if (isVisible) { return VisibleIdentitySplitLine.INSTANCE; } return InvisibleIdentitySplitLine.INSTANCE; } else { - return new SplitLine(positionMapper, isVisible); + return new SplitLine(lineBreakData, isVisible); } } @@ -1294,10 +1460,22 @@ export class IdentityLinesCollection implements IViewModelLinesCollection { return false; } - public setWrappingSettings(_wrappingIndent: WrappingIndent, _wrappingColumn: number, _columnsForFullWidthChar: number): boolean { + public setWrappingSettings(_fontInfo: FontInfo, _wrappingAlgorithm: 'monospace' | 'dom', _wrappingColumn: number, _wrappingIndent: WrappingIndent): boolean { return false; } + public createLineBreaksComputer(): ILineBreaksComputer { + let result: null[] = []; + return { + addRequest: (lineText: string, previousLineBreakData: LineBreakData | null) => { + result.push(null); + }, + finalize: () => { + return result; + } + }; + } + public onModelFlushed(): void { } @@ -1305,11 +1483,11 @@ export class IdentityLinesCollection implements IViewModelLinesCollection { return new viewEvents.ViewLinesDeletedEvent(fromLineNumber, toLineNumber); } - public onModelLinesInserted(_versionId: number, fromLineNumber: number, toLineNumber: number, _text: string[]): viewEvents.ViewLinesInsertedEvent | null { + public onModelLinesInserted(_versionId: number, fromLineNumber: number, toLineNumber: number, lineBreaks: (LineBreakData | null)[]): viewEvents.ViewLinesInsertedEvent | null { return new viewEvents.ViewLinesInsertedEvent(fromLineNumber, toLineNumber); } - public onModelLineChanged(_versionId: number, lineNumber: number, _newText: string): [boolean, viewEvents.ViewLinesChangedEvent | null, viewEvents.ViewLinesInsertedEvent | null, viewEvents.ViewLinesDeletedEvent | null] { + public onModelLineChanged(_versionId: number, lineNumber: number, lineBreakData: LineBreakData | null): [boolean, viewEvents.ViewLinesChangedEvent | null, viewEvents.ViewLinesInsertedEvent | null, viewEvents.ViewLinesDeletedEvent | null] { return [false, new viewEvents.ViewLinesChangedEvent(lineNumber, lineNumber), null, null]; } @@ -1320,9 +1498,6 @@ export class IdentityLinesCollection implements IViewModelLinesCollection { return this.model.getLineCount(); } - public warmUpLookupCache(_viewStartLineNumber: number, _viewEndLineNumber: number): void { - } - public getActiveIndentGuide(viewLineNumber: number, _minLineNumber: number, _maxLineNumber: number): IActiveIndentGuideInfo { return { startLineNumber: viewLineNumber, @@ -1364,6 +1539,7 @@ export class IdentityLinesCollection implements IViewModelLinesCollection { false, 1, lineContent.length + 1, + 0, lineTokens.inflate() ); } diff --git a/src/vs/editor/common/viewModel/viewEventHandler.ts b/src/vs/editor/common/viewModel/viewEventHandler.ts index 46bd71a86f..741d9ecb78 100644 --- a/src/vs/editor/common/viewModel/viewEventHandler.ts +++ b/src/vs/editor/common/viewModel/viewEventHandler.ts @@ -36,6 +36,9 @@ export class ViewEventHandler extends Disposable { public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { return false; } + public onContentSizeChanged(e: viewEvents.ViewContentSizeChangedEvent): boolean { + return false; + } public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { return false; } @@ -69,6 +72,9 @@ export class ViewEventHandler extends Disposable { public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean { return false; } + public onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean { + return false; + } public onTokensChanged(e: viewEvents.ViewTokensChangedEvent): boolean { return false; } @@ -78,9 +84,6 @@ export class ViewEventHandler extends Disposable { public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean { return false; } - public onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean { - return false; - } // --- end event handlers @@ -99,6 +102,12 @@ export class ViewEventHandler extends Disposable { } break; + case viewEvents.ViewEventType.ViewContentSizeChanged: + if (this.onContentSizeChanged(e)) { + shouldRender = true; + } + break; + case viewEvents.ViewEventType.ViewCursorStateChanged: if (this.onCursorStateChanged(e)) { shouldRender = true; @@ -171,6 +180,12 @@ export class ViewEventHandler extends Disposable { } break; + case viewEvents.ViewEventType.ViewThemeChanged: + if (this.onThemeChanged(e)) { + shouldRender = true; + } + break; + case viewEvents.ViewEventType.ViewTokensColorsChanged: if (this.onTokensColorsChanged(e)) { shouldRender = true; @@ -183,12 +198,6 @@ export class ViewEventHandler extends Disposable { } break; - case viewEvents.ViewEventType.ViewThemeChanged: - if (this.onThemeChanged(e)) { - shouldRender = true; - } - break; - default: console.info('View received unknown event: '); console.info(e); diff --git a/src/vs/editor/common/viewModel/viewModel.ts b/src/vs/editor/common/viewModel/viewModel.ts index 2aaa893db1..40820d7323 100644 --- a/src/vs/editor/common/viewModel/viewModel.ts +++ b/src/vs/editor/common/viewModel/viewModel.ts @@ -41,7 +41,7 @@ export class Viewport { export interface IViewLayout { - readonly scrollable: Scrollable; + getScrollable(): Scrollable; onMaxLineWidthChanged(width: number): void; @@ -174,6 +174,10 @@ export class ViewLineData { * The maximum allowed column at this view line. */ public readonly maxColumn: number; + /** + * The visible column at the start of the line (after the fauxIndent). + */ + public readonly startVisibleColumn: number; /** * The tokens at this view line. */ @@ -184,12 +188,14 @@ export class ViewLineData { continuesWithWrappedLine: boolean, minColumn: number, maxColumn: number, + startVisibleColumn: number, tokens: IViewLineTokens ) { this.content = content; this.continuesWithWrappedLine = continuesWithWrappedLine; this.minColumn = minColumn; this.maxColumn = maxColumn; + this.startVisibleColumn = startVisibleColumn; this.tokens = tokens; } } @@ -231,6 +237,10 @@ export class ViewLineRenderingData { * The tab size for this view model. */ public readonly tabSize: number; + /** + * The visible column at the start of the line (after the fauxIndent) + */ + public readonly startVisibleColumn: number; constructor( minColumn: number, @@ -241,7 +251,8 @@ export class ViewLineRenderingData { mightContainNonBasicASCII: boolean, tokens: IViewLineTokens, inlineDecorations: InlineDecoration[], - tabSize: number + tabSize: number, + startVisibleColumn: number ) { this.minColumn = minColumn; this.maxColumn = maxColumn; @@ -254,6 +265,7 @@ export class ViewLineRenderingData { this.tokens = tokens; this.inlineDecorations = inlineDecorations; this.tabSize = tabSize; + this.startVisibleColumn = startVisibleColumn; } public static isBasicASCII(lineContent: string, mightContainNonBasicASCII: boolean): boolean { diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index 4a2cd356ad..54b5155a77 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -9,7 +9,7 @@ import * as strings from 'vs/base/common/strings'; import { ConfigurationChangedEvent, EDITOR_FONT_DEFAULTS, EditorOption } from 'vs/editor/common/config/editorOptions'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; -import * as editorCommon from 'vs/editor/common/editorCommon'; +import { IConfiguration, IViewState } from 'vs/editor/common/editorCommon'; import { EndOfLinePreference, IActiveIndentGuideInfo, ITextModel, TrackedRangeStickiness, TextModelResolvedOptions } from 'vs/editor/common/model'; import { ModelDecorationOverviewRulerOptions, ModelDecorationMinimapOptions } from 'vs/editor/common/model/textModel'; import * as textModelEvents from 'vs/editor/common/model/textModelEvents'; @@ -18,8 +18,7 @@ import { tokenizeLineToHTML } from 'vs/editor/common/modes/textToHtmlTokenizer'; import { MinimapTokensColorTracker } from 'vs/editor/common/viewModel/minimapTokensColorTracker'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { ViewLayout } from 'vs/editor/common/viewLayout/viewLayout'; -import { CharacterHardWrappingLineMapperFactory } from 'vs/editor/common/viewModel/characterHardWrappingLineMapper'; -import { IViewModelLinesCollection, IdentityLinesCollection, SplitLinesCollection } from 'vs/editor/common/viewModel/splitLinesCollection'; +import { IViewModelLinesCollection, IdentityLinesCollection, SplitLinesCollection, ILineBreaksComputerFactory } from 'vs/editor/common/viewModel/splitLinesCollection'; import { ICoordinatesConverter, IOverviewRulerDecorations, IViewModel, MinimapLinesRenderingData, ViewLineData, ViewLineRenderingData, ViewModelDecoration } from 'vs/editor/common/viewModel/viewModel'; import { ViewModelDecorations } from 'vs/editor/common/viewModel/viewModelDecorations'; import { ITheme } from 'vs/platform/theme/common/themeService'; @@ -31,7 +30,7 @@ const USE_IDENTITY_LINES_COLLECTION = true; export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel { private readonly editorId: number; - private readonly configuration: editorCommon.IConfiguration; + private readonly configuration: IConfiguration; private readonly model: ITextModel; private readonly _tokenizeViewportSoon: RunOnceScheduler; private hasFocus: boolean; @@ -43,7 +42,14 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel public readonly viewLayout: ViewLayout; private readonly decorations: ViewModelDecorations; - constructor(editorId: number, configuration: editorCommon.IConfiguration, model: ITextModel, scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable) { + constructor( + editorId: number, + configuration: IConfiguration, + model: ITextModel, + domLineBreaksComputerFactory: ILineBreaksComputerFactory, + monospaceLineBreaksComputerFactory: ILineBreaksComputerFactory, + scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable + ) { super(); this.editorId = editorId; @@ -61,25 +67,19 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel } else { const options = this.configuration.options; - const wrappingInfo = options.get(EditorOption.wrappingInfo); const fontInfo = options.get(EditorOption.fontInfo); - const wordWrapBreakAfterCharacters = options.get(EditorOption.wordWrapBreakAfterCharacters); - const wordWrapBreakBeforeCharacters = options.get(EditorOption.wordWrapBreakBeforeCharacters); - const wordWrapBreakObtrusiveCharacters = options.get(EditorOption.wordWrapBreakObtrusiveCharacters); + const wrappingAlgorithm = options.get(EditorOption.wrappingAlgorithm); + const wrappingInfo = options.get(EditorOption.wrappingInfo); const wrappingIndent = options.get(EditorOption.wrappingIndent); - let hardWrappingLineMapperFactory = new CharacterHardWrappingLineMapperFactory( - wordWrapBreakBeforeCharacters, - wordWrapBreakAfterCharacters, - wordWrapBreakObtrusiveCharacters - ); - this.lines = new SplitLinesCollection( this.model, - hardWrappingLineMapperFactory, + domLineBreaksComputerFactory, + monospaceLineBreaksComputerFactory, + fontInfo, this.model.getOptions().tabSize, + wrappingAlgorithm, wrappingInfo.wrappingColumn, - fontInfo.typicalFullwidthCharacterWidth / fontInfo.typicalHalfwidthCharacterWidth, wrappingIndent ); } @@ -100,6 +100,15 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel } })); + this._register(this.viewLayout.onDidContentSizeChange((e) => { + try { + const eventsCollector = this._beginEmit(); + eventsCollector.emit(new viewEvents.ViewContentSizeChangedEvent(e)); + } finally { + this._endEmit(); + } + })); + this.decorations = new ViewModelDecorations(this.editorId, this.model, this.configuration, this.lines, this.coordinatesConverter); this._registerModelEvents(); @@ -155,11 +164,12 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel let restorePreviousViewportStart = false; const options = this.configuration.options; - const wrappingInfo = options.get(EditorOption.wrappingInfo); const fontInfo = options.get(EditorOption.fontInfo); + const wrappingAlgorithm = options.get(EditorOption.wrappingAlgorithm); + const wrappingInfo = options.get(EditorOption.wrappingInfo); const wrappingIndent = options.get(EditorOption.wrappingIndent); - if (this.lines.setWrappingSettings(wrappingIndent, wrappingInfo.wrappingColumn, fontInfo.typicalFullwidthCharacterWidth / fontInfo.typicalHalfwidthCharacterWidth)) { + if (this.lines.setWrappingSettings(fontInfo, wrappingAlgorithm, wrappingInfo.wrappingColumn, wrappingIndent)) { eventsCollector.emit(new viewEvents.ViewFlushedEvent()); eventsCollector.emit(new viewEvents.ViewLineMappingChangedEvent()); eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent()); @@ -200,8 +210,26 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel const changes = e.changes; const versionId = e.versionId; - for (let j = 0, lenJ = changes.length; j < lenJ; j++) { - const change = changes[j]; + // Do a first pass to compute line mappings, and a second pass to actually interpret them + const lineBreaksComputer = this.lines.createLineBreaksComputer(); + for (const change of changes) { + switch (change.changeType) { + case textModelEvents.RawContentChangedType.LinesInserted: { + for (const line of change.detail) { + lineBreaksComputer.addRequest(line, null); + } + break; + } + case textModelEvents.RawContentChangedType.LineChanged: { + lineBreaksComputer.addRequest(change.detail, null); + break; + } + } + } + const lineBreaks = lineBreaksComputer.finalize(); + let lineBreaksOffset = 0; + + for (const change of changes) { switch (change.changeType) { case textModelEvents.RawContentChangedType.Flush: { @@ -222,7 +250,10 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel break; } case textModelEvents.RawContentChangedType.LinesInserted: { - const linesInsertedEvent = this.lines.onModelLinesInserted(versionId, change.fromLineNumber, change.toLineNumber, change.detail); + const insertedLineBreaks = lineBreaks.slice(lineBreaksOffset, lineBreaksOffset + change.detail.length); + lineBreaksOffset += change.detail.length; + + const linesInsertedEvent = this.lines.onModelLinesInserted(versionId, change.fromLineNumber, change.toLineNumber, insertedLineBreaks); if (linesInsertedEvent !== null) { eventsCollector.emit(linesInsertedEvent); this.viewLayout.onLinesInserted(linesInsertedEvent.fromLineNumber, linesInsertedEvent.toLineNumber); @@ -231,7 +262,10 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel break; } case textModelEvents.RawContentChangedType.LineChanged: { - const [lineMappingChanged, linesChangedEvent, linesInsertedEvent, linesDeletedEvent] = this.lines.onModelLineChanged(versionId, change.lineNumber, change.detail); + const changedLineBreakData = lineBreaks[lineBreaksOffset]; + lineBreaksOffset++; + + const [lineMappingChanged, linesChangedEvent, linesInsertedEvent, linesDeletedEvent] = this.lines.onModelLineChanged(versionId, change.lineNumber, changedLineBreakData); hadModelLineChangeThatChangedLineMapping = lineMappingChanged; if (linesChangedEvent) { eventsCollector.emit(linesChangedEvent); @@ -422,7 +456,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel ); } - public saveState(): editorCommon.IViewState { + public saveState(): IViewState { const compatViewState = this.viewLayout.saveState(); const scrollTop = compatViewState.scrollTop; @@ -437,7 +471,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel }; } - public reduceRestoreState(state: editorCommon.IViewState): { scrollLeft: number; scrollTop: number; } { + public reduceRestoreState(state: IViewState): { scrollLeft: number; scrollTop: number; } { if (typeof state.firstPosition === 'undefined') { // This is a view state serialized by an older version return this._reduceRestoreStateCompatibility(state); @@ -452,7 +486,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel }; } - private _reduceRestoreStateCompatibility(state: editorCommon.IViewState): { scrollLeft: number; scrollTop: number; } { + private _reduceRestoreStateCompatibility(state: IViewState): { scrollLeft: number; scrollTop: number; } { return { scrollLeft: state.scrollLeft, scrollTop: state.scrollTopWithoutViewZones! @@ -475,8 +509,6 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel * Gives a hint that a lot of requests are about to come in for these line numbers. */ public setViewport(startLineNumber: number, endLineNumber: number, centeredLineNumber: number): void { - this.lines.warmUpLookupCache(startLineNumber, endLineNumber); - this.viewportStartLine = startLineNumber; let position = this.coordinatesConverter.convertViewPositionToModelPosition(new Position(startLineNumber, this.getLineMinColumn(startLineNumber))); this.viewportStartLineTrackedRange = this.model._setTrackedRange(this.viewportStartLineTrackedRange, new Range(position.lineNumber, position.column, position.lineNumber, position.column), TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges); @@ -546,7 +578,8 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel mightContainNonBasicASCII, lineData.tokens, inlineDecorations, - tabSize + tabSize, + lineData.startVisibleColumn ); } diff --git a/src/vs/editor/contrib/bracketMatching/bracketMatching.ts b/src/vs/editor/contrib/bracketMatching/bracketMatching.ts index bc9e43f5ac..1fccc31054 100644 --- a/src/vs/editor/contrib/bracketMatching/bracketMatching.ts +++ b/src/vs/editor/contrib/bracketMatching/bracketMatching.ts @@ -13,7 +13,7 @@ import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorCon 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 * as editorCommon from 'vs/editor/common/editorCommon'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { IModelDeltaDecoration, OverviewRulerLane, TrackedRangeStickiness } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; @@ -103,7 +103,7 @@ class BracketsData { } } -export class BracketMatchingController extends Disposable implements editorCommon.IEditorContribution { +export class BracketMatchingController extends Disposable implements IEditorContribution { public static readonly ID = 'editor.contrib.bracketMatchingController'; public static get(editor: ICodeEditor): BracketMatchingController { diff --git a/src/vs/editor/contrib/codeAction/codeAction.ts b/src/vs/editor/contrib/codeAction/codeAction.ts index 408e429a32..87a66792e6 100644 --- a/src/vs/editor/contrib/codeAction/codeAction.ts +++ b/src/vs/editor/contrib/codeAction/codeAction.ts @@ -6,16 +6,16 @@ import { equals, flatten, isNonEmptyArray, mergeSort } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { illegalArgument, isPromiseCanceledError, onUnexpectedExternalError } from 'vs/base/common/errors'; +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; +import { TextModelCancellationTokenSource } from 'vs/editor/browser/core/editorState'; import { registerLanguageCommand } from 'vs/editor/browser/editorExtensions'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { ITextModel } from 'vs/editor/common/model'; -import { CodeAction, CodeActionContext, CodeActionProviderRegistry, CodeActionTrigger as CodeActionTriggerKind } from 'vs/editor/common/modes'; +import * as modes from 'vs/editor/common/modes'; import { IModelService } from 'vs/editor/common/services/modelService'; import { CodeActionFilter, CodeActionKind, CodeActionTrigger, filtersAction, mayIncludeActionsOfKind } from './types'; -import { TextModelCancellationTokenSource } from 'vs/editor/browser/core/editorState'; -import { DisposableStore, IDisposable, Disposable } from 'vs/base/common/lifecycle'; export const codeActionCommandId = 'editor.action.codeAction'; export const refactorCommandId = 'editor.action.refactor'; @@ -24,14 +24,14 @@ export const organizeImportsCommandId = 'editor.action.organizeImports'; export const fixAllCommandId = 'editor.action.fixAll'; export interface CodeActionSet extends IDisposable { - readonly validActions: readonly CodeAction[]; - readonly allActions: readonly CodeAction[]; + readonly validActions: readonly modes.CodeAction[]; + readonly allActions: readonly modes.CodeAction[]; readonly hasAutoFix: boolean; } class ManagedCodeActionSet extends Disposable implements CodeActionSet { - private static codeActionsComparator(a: CodeAction, b: CodeAction): number { + private static codeActionsComparator(a: modes.CodeAction, b: modes.CodeAction): number { if (isNonEmptyArray(a.diagnostics)) { if (isNonEmptyArray(b.diagnostics)) { return a.diagnostics[0].message.localeCompare(b.diagnostics[0].message); @@ -45,10 +45,10 @@ class ManagedCodeActionSet extends Disposable implements CodeActionSet { } } - public readonly validActions: readonly CodeAction[]; - public readonly allActions: readonly CodeAction[]; + public readonly validActions: readonly modes.CodeAction[]; + public readonly allActions: readonly modes.CodeAction[]; - public constructor(actions: readonly CodeAction[], disposables: DisposableStore) { + public constructor(actions: readonly modes.CodeAction[], disposables: DisposableStore) { super(); this._register(disposables); this.allActions = mergeSort([...actions], ManagedCodeActionSet.codeActionsComparator); @@ -68,9 +68,9 @@ export function getCodeActions( ): Promise { const filter = trigger.filter || {}; - const codeActionContext: CodeActionContext = { + const codeActionContext: modes.CodeActionContext = { only: filter.include?.value, - trigger: trigger.type === 'manual' ? CodeActionTriggerKind.Manual : CodeActionTriggerKind.Automatic + trigger: trigger.type, }; const cts = new TextModelCancellationTokenSource(model, token); @@ -94,8 +94,8 @@ export function getCodeActions( } }); - const listener = CodeActionProviderRegistry.onDidChange(() => { - const newProviders = CodeActionProviderRegistry.all(model); + const listener = modes.CodeActionProviderRegistry.onDidChange(() => { + const newProviders = modes.CodeActionProviderRegistry.all(model); if (!equals(newProviders, providers)) { cts.cancel(); } @@ -114,7 +114,7 @@ function getCodeActionProviders( model: ITextModel, filter: CodeActionFilter ) { - return CodeActionProviderRegistry.all(model) + return modes.CodeActionProviderRegistry.all(model) // Don't include providers that we know will not return code actions of interest .filter(provider => { if (!provider.providedCodeActionKinds) { @@ -125,7 +125,7 @@ function getCodeActionProviders( }); } -registerLanguageCommand('_executeCodeActionProvider', async function (accessor, args): Promise> { +registerLanguageCommand('_executeCodeActionProvider', async function (accessor, args): Promise> { const { resource, rangeOrSelection, kind } = args; if (!(resource instanceof URI)) { throw illegalArgument(); @@ -149,7 +149,7 @@ registerLanguageCommand('_executeCodeActionProvider', async function (accessor, const codeActionSet = await getCodeActions( model, validatedRangeOrSelection, - { type: 'manual', filter: { includeSourceActions: true, include: kind && kind.value ? new CodeActionKind(kind.value) : undefined } }, + { type: modes.CodeActionTriggerType.Manual, filter: { includeSourceActions: true, include: kind && kind.value ? new CodeActionKind(kind.value) : undefined } }, CancellationToken.None); setTimeout(() => codeActionSet.dispose(), 100); diff --git a/src/vs/editor/contrib/codeAction/codeActionCommands.ts b/src/vs/editor/contrib/codeAction/codeActionCommands.ts index f984998d89..611812ad3d 100644 --- a/src/vs/editor/contrib/codeAction/codeActionCommands.ts +++ b/src/vs/editor/contrib/codeAction/codeActionCommands.ts @@ -15,22 +15,21 @@ import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { IPosition } from 'vs/editor/common/core/position'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { CodeAction } from 'vs/editor/common/modes'; -import { CodeActionSet, refactorCommandId, sourceActionCommandId, codeActionCommandId, organizeImportsCommandId, fixAllCommandId } from 'vs/editor/contrib/codeAction/codeAction'; +import { CodeAction, CodeActionTriggerType } from 'vs/editor/common/modes'; +import { codeActionCommandId, CodeActionSet, fixAllCommandId, organizeImportsCommandId, refactorCommandId, sourceActionCommandId } from 'vs/editor/contrib/codeAction/codeAction'; import { CodeActionUi } from 'vs/editor/contrib/codeAction/codeActionUi'; import { MessageController } from 'vs/editor/contrib/message/messageController'; import * as nls from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IMarkerService } from 'vs/platform/markers/common/markers'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IEditorProgressService } from 'vs/platform/progress/common/progress'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { CodeActionModel, CodeActionsState, SUPPORTED_CODE_ACTIONS } from './codeActionModel'; -import { CodeActionAutoApply, CodeActionFilter, CodeActionKind, CodeActionTrigger, CodeActionCommandArgs } from './types'; +import { CodeActionAutoApply, CodeActionCommandArgs, CodeActionFilter, CodeActionKind, CodeActionTrigger } from './types'; function contextKeyForSupportedActions(kind: CodeActionKind) { return ContextKeyExpr.regex( @@ -83,10 +82,6 @@ export class QuickFixController extends Disposable implements IEditorContributio @IMarkerService markerService: IMarkerService, @IContextKeyService contextKeyService: IContextKeyService, @IEditorProgressService progressService: IEditorProgressService, - @IContextMenuService contextMenuService: IContextMenuService, - @IKeybindingService keybindingService: IKeybindingService, - @ICommandService private readonly _commandService: ICommandService, - @IBulkEditService private readonly _bulkEditService: IBulkEditService, @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); @@ -102,7 +97,7 @@ export class QuickFixController extends Disposable implements IEditorContributio await this._applyCodeAction(action); } finally { if (retrigger) { - this._trigger({ type: 'auto', filter: {} }); + this._trigger({ type: CodeActionTriggerType.Auto, filter: {} }); } } } @@ -114,8 +109,8 @@ export class QuickFixController extends Disposable implements IEditorContributio this._ui.getValue().update(newState); } - public showCodeActions(actions: CodeActionSet, at: IAnchor | IPosition) { - return this._ui.getValue().showCodeActionList(actions, at, { includeDisabledActions: false }); + public showCodeActions(trigger: CodeActionTrigger, actions: CodeActionSet, at: IAnchor | IPosition) { + return this._ui.getValue().showCodeActionList(trigger, actions, at, { includeDisabledActions: false }); } public manualTriggerAtCurrentPosition( @@ -129,7 +124,7 @@ export class QuickFixController extends Disposable implements IEditorContributio MessageController.get(this._editor).closeMessage(); const triggerPosition = this._editor.getPosition(); - this._trigger({ type: 'manual', filter, autoApply, context: { notAvailableMessage, position: triggerPosition } }); + this._trigger({ type: CodeActionTriggerType.Manual, filter, autoApply, context: { notAvailableMessage, position: triggerPosition } }); } private _trigger(trigger: CodeActionTrigger) { @@ -137,21 +132,41 @@ export class QuickFixController extends Disposable implements IEditorContributio } private _applyCodeAction(action: CodeAction): Promise { - return this._instantiationService.invokeFunction(applyCodeAction, action, this._bulkEditService, this._commandService, this._editor); + return this._instantiationService.invokeFunction(applyCodeAction, action, this._editor); } } export async function applyCodeAction( accessor: ServicesAccessor, action: CodeAction, - bulkEditService: IBulkEditService, - commandService: ICommandService, editor?: ICodeEditor, ): Promise { + const bulkEditService = accessor.get(IBulkEditService); + const commandService = accessor.get(ICommandService); + const telemetryService = accessor.get(ITelemetryService); const notificationService = accessor.get(INotificationService); + + type ApplyCodeActionEvent = { + codeActionTitle: string; + codeActionKind: string | undefined; + codeActionIsPreferred: boolean; + }; + type ApplyCodeEventClassification = { + codeActionTitle: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + codeActionKind: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + codeActionIsPreferred: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + }; + + telemetryService.publicLog2('codeAction.applyCodeAction', { + codeActionTitle: action.title, + codeActionKind: action.kind, + codeActionIsPreferred: !!action.isPreferred, + }); + if (action.edit) { await bulkEditService.apply(action.edit, { editor }); } + if (action.command) { try { await commandService.executeCommand(action.command.id, ...(action.command.arguments || [])); @@ -161,7 +176,6 @@ export async function applyCodeAction( typeof message === 'string' ? message : nls.localize('applyCodeActionFailed', "An unknown error occurred while applying the code action")); - } } } diff --git a/src/vs/editor/contrib/codeAction/codeActionMenu.ts b/src/vs/editor/contrib/codeAction/codeActionMenu.ts index b30ec28bc7..dbe87f123d 100644 --- a/src/vs/editor/contrib/codeAction/codeActionMenu.ts +++ b/src/vs/editor/contrib/codeAction/codeActionMenu.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { getDomNodePagePosition } from 'vs/base/browser/dom'; +import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { IAnchor } from 'vs/base/browser/ui/contextview/contextview'; -import { Action } from 'vs/base/common/actions'; +import { Action, IAction } from 'vs/base/common/actions'; import { canceled } from 'vs/base/common/errors'; import { ResolvedKeybinding } from 'vs/base/common/keyCodes'; import { Lazy } from 'vs/base/common/lazy'; @@ -13,9 +14,9 @@ import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { ScrollType } from 'vs/editor/common/editorCommon'; -import { CodeAction } from 'vs/editor/common/modes'; -import { CodeActionSet, refactorCommandId, sourceActionCommandId, codeActionCommandId, organizeImportsCommandId, fixAllCommandId } from 'vs/editor/contrib/codeAction/codeAction'; -import { CodeActionAutoApply, CodeActionCommandArgs, CodeActionKind } from 'vs/editor/contrib/codeAction/types'; +import { CodeAction, CodeActionProviderRegistry } from 'vs/editor/common/modes'; +import { codeActionCommandId, CodeActionSet, fixAllCommandId, organizeImportsCommandId, refactorCommandId, sourceActionCommandId } from 'vs/editor/contrib/codeAction/codeAction'; +import { CodeActionAutoApply, CodeActionCommandArgs, CodeActionKind, CodeActionTrigger } from 'vs/editor/contrib/codeAction/types'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; @@ -67,7 +68,7 @@ export class CodeActionMenu extends Disposable { return this._visible; } - public async show(codeActions: CodeActionSet, at: IAnchor | IPosition, options: CodeActionShowOptions): Promise { + public async show(trigger: CodeActionTrigger, codeActions: CodeActionSet, at: IAnchor | IPosition, options: CodeActionShowOptions): Promise { const actionsToShow = options.includeDisabledActions ? codeActions.allActions : codeActions.validActions; if (!actionsToShow.length) { this._visible = false; @@ -83,8 +84,7 @@ export class CodeActionMenu extends Disposable { this._visible = true; this._showingActions.value = codeActions; - const menuActions = actionsToShow.map(action => - new CodeActionAction(action, () => this._delegate.onSelectCodeAction(action))); + const menuActions = this.getMenuActions(trigger, actionsToShow); const anchor = Position.isIPosition(at) ? this._toCoords(at) : at || { x: 0, y: 0 }; const resolver = this._keybindingResolver.getResolver(); @@ -101,6 +101,31 @@ export class CodeActionMenu extends Disposable { }); } + private getMenuActions(trigger: CodeActionTrigger, actionsToShow: readonly CodeAction[]): IAction[] { + const toCodeActionAction = (action: CodeAction): CodeActionAction => new CodeActionAction(action, () => this._delegate.onSelectCodeAction(action)); + + const result: IAction[] = actionsToShow + .map(toCodeActionAction); + + + const model = this._editor.getModel(); + if (model && result.length) { + for (const provider of CodeActionProviderRegistry.all(model)) { + if (provider._getAdditionalMenuItems) { + const items = provider._getAdditionalMenuItems({ trigger: trigger.type, only: trigger.filter?.include?.value }, actionsToShow); + if (items.length) { + result.push(new Separator(), ...items.map(command => toCodeActionAction({ + title: command.title, + command: command, + }))); + } + } + } + } + + return result; + } + private _toCoords(position: IPosition): { x: number, y: number } { if (!this._editor.hasModel()) { return { x: 0, y: 0 }; diff --git a/src/vs/editor/contrib/codeAction/codeActionModel.ts b/src/vs/editor/contrib/codeAction/codeActionModel.ts index b6203ae7f8..705a4944a6 100644 --- a/src/vs/editor/contrib/codeAction/codeActionModel.ts +++ b/src/vs/editor/contrib/codeAction/codeActionModel.ts @@ -11,7 +11,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; 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 { CodeActionProviderRegistry } from 'vs/editor/common/modes'; +import { CodeActionProviderRegistry, CodeActionTriggerType } from 'vs/editor/common/modes'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IMarkerService } from 'vs/platform/markers/common/markers'; import { IEditorProgressService } from 'vs/platform/progress/common/progress'; @@ -56,14 +56,14 @@ class CodeActionOracle extends Disposable { if (resources.some(resource => isEqual(resource, model.uri))) { this._autoTriggerTimer.cancelAndSet(() => { - this.trigger({ type: 'auto' }); + this.trigger({ type: CodeActionTriggerType.Auto }); }, this._delay); } } private _onCursorChange(): void { this._autoTriggerTimer.cancelAndSet(() => { - this.trigger({ type: 'auto' }); + this.trigger({ type: CodeActionTriggerType.Auto }); }, this._delay); } @@ -88,7 +88,7 @@ class CodeActionOracle extends Disposable { } const model = this._editor.getModel(); const selection = this._editor.getSelection(); - if (selection.isEmpty() && trigger.type === 'auto') { + if (selection.isEmpty() && trigger.type === CodeActionTriggerType.Auto) { const { lineNumber, column } = selection.getPosition(); const line = model.getLineContent(lineNumber); if (line.length === 0) { @@ -214,14 +214,14 @@ export class CodeActionModel extends Disposable { } const actions = createCancelablePromise(token => getCodeActions(model, trigger.selection, trigger.trigger, token)); - if (this._progressService && trigger.trigger.type === 'manual') { + if (this._progressService && trigger.trigger.type === CodeActionTriggerType.Manual) { this._progressService.showWhile(actions, 250); } this.setState(new CodeActionsState.Triggered(trigger.trigger, trigger.selection, trigger.position, actions)); }, undefined); - this._codeActionOracle.value.trigger({ type: 'auto' }); + this._codeActionOracle.value.trigger({ type: CodeActionTriggerType.Auto }); } else { this._supportedCodeActions.reset(); } diff --git a/src/vs/editor/contrib/codeAction/codeActionUi.ts b/src/vs/editor/contrib/codeAction/codeActionUi.ts index 5aa3970814..cb550d8827 100644 --- a/src/vs/editor/contrib/codeAction/codeActionUi.ts +++ b/src/vs/editor/contrib/codeAction/codeActionUi.ts @@ -10,14 +10,14 @@ import { Lazy } from 'vs/base/common/lazy'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IPosition } from 'vs/editor/common/core/position'; -import { CodeAction } from 'vs/editor/common/modes'; +import { CodeAction, CodeActionTriggerType } from 'vs/editor/common/modes'; import { CodeActionSet } from 'vs/editor/contrib/codeAction/codeAction'; import { MessageController } from 'vs/editor/contrib/message/messageController'; -import { CodeActionsState } from './codeActionModel'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { CodeActionMenu, CodeActionShowOptions } from './codeActionMenu'; +import { CodeActionsState } from './codeActionModel'; import { LightBulbWidget } from './lightBulbWidget'; import { CodeActionAutoApply, CodeActionTrigger } from './types'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export class CodeActionUi extends Disposable { @@ -46,7 +46,7 @@ export class CodeActionUi extends Disposable { this._lightBulbWidget = new Lazy(() => { const widget = this._register(instantiationService.createInstance(LightBulbWidget, this._editor, quickFixActionId, preferredFixActionId)); - this._register(widget.onClick(e => this.showCodeActionList(e.actions, e, { includeDisabledActions: false }))); + this._register(widget.onClick(e => this.showCodeActionList(e.trigger, e.actions, e, { includeDisabledActions: false }))); return widget; }); } @@ -65,9 +65,9 @@ export class CodeActionUi extends Disposable { return; } - this._lightBulbWidget.getValue().update(actions, newState.position); + this._lightBulbWidget.getValue().update(actions, newState.trigger, newState.position); - if (newState.trigger.type === 'manual') { + if (newState.trigger.type === CodeActionTriggerType.Manual) { if (newState.trigger.filter?.include) { // Triggered for specific scope // Check to see if we want to auto apply. @@ -103,7 +103,7 @@ export class CodeActionUi extends Disposable { } this._activeCodeActions.value = actions; - this._codeActionWidget.getValue().show(actions, newState.position, { includeDisabledActions }); + this._codeActionWidget.getValue().show(newState.trigger, actions, newState.position, { includeDisabledActions }); } else { // auto magically triggered if (this._codeActionWidget.getValue().isVisible) { @@ -143,7 +143,7 @@ export class CodeActionUi extends Disposable { return undefined; } - public async showCodeActionList(actions: CodeActionSet, at: IAnchor | IPosition, options: CodeActionShowOptions): Promise { - this._codeActionWidget.getValue().show(actions, at, options); + public async showCodeActionList(trigger: CodeActionTrigger, actions: CodeActionSet, at: IAnchor | IPosition, options: CodeActionShowOptions): Promise { + this._codeActionWidget.getValue().show(trigger, actions, at, options); } } diff --git a/src/vs/editor/contrib/codeAction/lightBulbWidget.ts b/src/vs/editor/contrib/codeAction/lightBulbWidget.ts index ff2697dfa3..b04705e853 100644 --- a/src/vs/editor/contrib/codeAction/lightBulbWidget.ts +++ b/src/vs/editor/contrib/codeAction/lightBulbWidget.ts @@ -18,6 +18,7 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { editorLightBulbForeground, editorLightBulbAutoFixForeground } from 'vs/platform/theme/common/colorRegistry'; import { Gesture } from 'vs/base/browser/touch'; +import type { CodeActionTrigger } from 'vs/editor/contrib/codeAction/types'; namespace LightBulbState { @@ -33,6 +34,7 @@ namespace LightBulbState { constructor( public readonly actions: CodeActionSet, + public readonly trigger: CodeActionTrigger, public readonly editorPosition: IPosition, public readonly widgetPosition: IContentWidgetPosition, ) { } @@ -48,7 +50,7 @@ export class LightBulbWidget extends Disposable implements IContentWidget { private readonly _domNode: HTMLDivElement; - private readonly _onClick = this._register(new Emitter<{ x: number; y: number; actions: CodeActionSet; }>()); + private readonly _onClick = this._register(new Emitter<{ x: number; y: number; actions: CodeActionSet; trigger: CodeActionTrigger }>()); public readonly onClick = this._onClick.event; private _state: LightBulbState.State = LightBulbState.Hidden; @@ -95,7 +97,8 @@ export class LightBulbWidget extends Disposable implements IContentWidget { this._onClick.fire({ x: e.posx, y: top + height + pad, - actions: this.state.actions + actions: this.state.actions, + trigger: this.state.trigger, }); })); this._register(dom.addDisposableListener(this._domNode, 'mouseenter', (e: MouseEvent) => { @@ -107,7 +110,7 @@ export class LightBulbWidget extends Disposable implements IContentWidget { // showings until mouse is released this.hide(); const monitor = new GlobalMouseMoveMonitor(); - monitor.startMonitoring(standardMouseMoveMerger, () => { }, () => { + monitor.startMonitoring(e.buttons, standardMouseMoveMerger, () => { }, () => { monitor.dispose(); }); })); @@ -139,7 +142,7 @@ export class LightBulbWidget extends Disposable implements IContentWidget { return this._state.type === LightBulbState.Type.Showing ? this._state.widgetPosition : null; } - public update(actions: CodeActionSet, atPosition: IPosition) { + public update(actions: CodeActionSet, trigger: CodeActionTrigger, atPosition: IPosition) { if (actions.validActions.length <= 0) { return this.hide(); } @@ -177,7 +180,7 @@ export class LightBulbWidget extends Disposable implements IContentWidget { } } - this.state = new LightBulbState.Showing(actions, atPosition, { + this.state = new LightBulbState.Showing(actions, trigger, atPosition, { position: { lineNumber: effectiveLineNumber, column: 1 }, preference: LightBulbWidget._posPref }); diff --git a/src/vs/editor/contrib/codeAction/test/codeAction.test.ts b/src/vs/editor/contrib/codeAction/test/codeAction.test.ts index fd3010577c..f52ecb2b0a 100644 --- a/src/vs/editor/contrib/codeAction/test/codeAction.test.ts +++ b/src/vs/editor/contrib/codeAction/test/codeAction.test.ts @@ -69,7 +69,7 @@ suite('CodeAction', () => { bcd: { diagnostics: [], edit: new class implements modes.WorkspaceEdit { - edits!: modes.ResourceTextEdit[]; + edits!: modes.WorkspaceTextEdit[]; }, title: 'abc' } @@ -125,7 +125,7 @@ suite('CodeAction', () => { testData.tsLint.abc ]; - const { validActions: actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'manual' }, CancellationToken.None); + const { validActions: actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: modes.CodeActionTriggerType.Manual }, CancellationToken.None); assert.equal(actions.length, 6); assert.deepEqual(actions, expected); }); @@ -140,20 +140,20 @@ suite('CodeAction', () => { disposables.add(modes.CodeActionProviderRegistry.register('fooLang', provider)); { - const { validActions: actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { include: new CodeActionKind('a') } }, CancellationToken.None); + const { validActions: actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: modes.CodeActionTriggerType.Auto, filter: { include: new CodeActionKind('a') } }, CancellationToken.None); assert.equal(actions.length, 2); assert.strictEqual(actions[0].title, 'a'); assert.strictEqual(actions[1].title, 'a.b'); } { - const { validActions: actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { include: new CodeActionKind('a.b') } }, CancellationToken.None); + const { validActions: actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: modes.CodeActionTriggerType.Auto, filter: { include: new CodeActionKind('a.b') } }, CancellationToken.None); assert.equal(actions.length, 1); assert.strictEqual(actions[0].title, 'a.b'); } { - const { validActions: actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { include: new CodeActionKind('a.b.c') } }, CancellationToken.None); + const { validActions: actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: modes.CodeActionTriggerType.Auto, filter: { include: new CodeActionKind('a.b.c') } }, CancellationToken.None); assert.equal(actions.length, 0); } }); @@ -172,7 +172,7 @@ suite('CodeAction', () => { disposables.add(modes.CodeActionProviderRegistry.register('fooLang', provider)); - const { validActions: actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { include: new CodeActionKind('a') } }, CancellationToken.None); + const { validActions: actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: modes.CodeActionTriggerType.Auto, filter: { include: new CodeActionKind('a') } }, CancellationToken.None); assert.equal(actions.length, 1); assert.strictEqual(actions[0].title, 'a'); }); @@ -186,13 +186,13 @@ suite('CodeAction', () => { disposables.add(modes.CodeActionProviderRegistry.register('fooLang', provider)); { - const { validActions: actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto' }, CancellationToken.None); + const { validActions: actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: modes.CodeActionTriggerType.Auto }, CancellationToken.None); assert.equal(actions.length, 1); assert.strictEqual(actions[0].title, 'b'); } { - const { validActions: actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { include: CodeActionKind.Source, includeSourceActions: true } }, CancellationToken.None); + const { validActions: actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: modes.CodeActionTriggerType.Auto, filter: { include: CodeActionKind.Source, includeSourceActions: true } }, CancellationToken.None); assert.equal(actions.length, 1); assert.strictEqual(actions[0].title, 'a'); } @@ -209,7 +209,7 @@ suite('CodeAction', () => { { const { validActions: actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { - type: 'auto', filter: { + type: modes.CodeActionTriggerType.Auto, filter: { include: CodeActionKind.Source.append('test'), excludes: [CodeActionKind.Source], includeSourceActions: true, @@ -234,7 +234,7 @@ suite('CodeAction', () => { disposables.add(modes.CodeActionProviderRegistry.register('fooLang', provider)); const { validActions: actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { - type: 'auto', + type: modes.CodeActionTriggerType.Auto, filter: { include: CodeActionKind.QuickFix } diff --git a/src/vs/editor/contrib/codeAction/test/codeActionModel.test.ts b/src/vs/editor/contrib/codeAction/test/codeActionModel.test.ts index d0b480a33f..38f6fd9c82 100644 --- a/src/vs/editor/contrib/codeAction/test/codeActionModel.test.ts +++ b/src/vs/editor/contrib/codeAction/test/codeActionModel.test.ts @@ -59,7 +59,7 @@ suite('CodeActionModel', () => { disposables.add(model.onDidChangeState((e: CodeActionsState.State) => { assertType(e.type === CodeActionsState.Type.Triggered); - assert.strictEqual(e.trigger.type, 'auto'); + assert.strictEqual(e.trigger.type, modes.CodeActionTriggerType.Auto); assert.ok(e.actions); e.actions.then(fixes => { @@ -100,7 +100,7 @@ suite('CodeActionModel', () => { disposables.add(model.onDidChangeState((e: CodeActionsState.State) => { assertType(e.type === CodeActionsState.Type.Triggered); - assert.equal(e.trigger.type, 'auto'); + assert.equal(e.trigger.type, modes.CodeActionTriggerType.Auto); assert.ok(e.actions); e.actions.then(fixes => { model.dispose(); @@ -138,7 +138,7 @@ suite('CodeActionModel', () => { disposables.add(model.onDidChangeState((e: CodeActionsState.State) => { assertType(e.type === CodeActionsState.Type.Triggered); - assert.equal(e.trigger.type, 'auto'); + assert.equal(e.trigger.type, modes.CodeActionTriggerType.Auto); const selection = e.rangeOrSelection; assert.deepEqual(selection.selectionStartLineNumber, 1); assert.deepEqual(selection.selectionStartColumn, 1); @@ -163,7 +163,7 @@ suite('CodeActionModel', () => { disposables.add(model.onDidChangeState((e: CodeActionsState.State) => { assertType(e.type === CodeActionsState.Type.Triggered); - assert.equal(e.trigger.type, 'auto'); + assert.equal(e.trigger.type, modes.CodeActionTriggerType.Auto); ++triggerCount; // give time for second trigger before completing test diff --git a/src/vs/editor/contrib/codeAction/types.ts b/src/vs/editor/contrib/codeAction/types.ts index c4ade7f152..e889f0af43 100644 --- a/src/vs/editor/contrib/codeAction/types.ts +++ b/src/vs/editor/contrib/codeAction/types.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { startsWith } from 'vs/base/common/strings'; -import { CodeAction } from 'vs/editor/common/modes'; +import { CodeAction, CodeActionTriggerType } from 'vs/editor/common/modes'; import { Position } from 'vs/editor/common/core/position'; export class CodeActionKind { @@ -102,7 +102,7 @@ export function filtersAction(filter: CodeActionFilter, action: CodeAction): boo } export interface CodeActionTrigger { - readonly type: 'auto' | 'manual'; + readonly type: CodeActionTriggerType; readonly filter?: CodeActionFilter; readonly autoApply?: CodeActionAutoApply; readonly context?: { diff --git a/src/vs/editor/contrib/codelens/codelensController.ts b/src/vs/editor/contrib/codelens/codelensController.ts index fc914f859a..7c52af14e6 100644 --- a/src/vs/editor/contrib/codelens/codelensController.ts +++ b/src/vs/editor/contrib/codelens/codelensController.ts @@ -7,9 +7,9 @@ import { CancelablePromise, RunOnceScheduler, createCancelablePromise, disposabl import { onUnexpectedError, onUnexpectedExternalError } from 'vs/base/common/errors'; import { toDisposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; import { StableEditorScrollState } from 'vs/editor/browser/core/editorState'; -import * as editorBrowser from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, MouseTargetType, IViewZoneChangeAccessor, IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import * as editorCommon from 'vs/editor/common/editorCommon'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { IModelDecorationsChangeAccessor } from 'vs/editor/common/model'; import { CodeLensProviderRegistry, CodeLens } from 'vs/editor/common/modes'; import { CodeLensModel, getCodeLensData, CodeLensItem } from 'vs/editor/contrib/codelens/codelens'; @@ -21,7 +21,7 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { createStyleSheet } from 'vs/base/browser/dom'; import { hash } from 'vs/base/common/hash'; -export class CodeLensContribution implements editorCommon.IEditorContribution { +export class CodeLensContribution implements IEditorContribution { public static readonly ID: string = 'css.editor.codeLens'; @@ -40,7 +40,7 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { private _detectVisibleLenses: RunOnceScheduler | undefined; constructor( - private readonly _editor: editorBrowser.ICodeEditor, + private readonly _editor: ICodeEditor, @ICommandService private readonly _commandService: ICommandService, @INotificationService private readonly _notificationService: INotificationService, @ICodeLensCache private readonly _codeLensCache: ICodeLensCache @@ -223,7 +223,7 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { } })); this._localToDispose.add(this._editor.onMouseUp(e => { - if (e.target.type !== editorBrowser.MouseTargetType.CONTENT_WIDGET) { + if (e.target.type !== MouseTargetType.CONTENT_WIDGET) { return; } let target = e.target.element; @@ -243,7 +243,7 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { scheduler.schedule(); } - private _disposeAllLenses(decChangeAccessor: IModelDecorationsChangeAccessor | undefined, viewZoneChangeAccessor: editorBrowser.IViewZoneChangeAccessor | undefined): void { + private _disposeAllLenses(decChangeAccessor: IModelDecorationsChangeAccessor | undefined, viewZoneChangeAccessor: IViewZoneChangeAccessor | undefined): void { const helper = new CodeLensHelper(); for (const lens of this._lenses) { lens.dispose(helper, viewZoneChangeAccessor); @@ -300,7 +300,7 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { groupsIndex++; codeLensIndex++; } else { - this._lenses.splice(codeLensIndex, 0, new CodeLensWidget(groups[groupsIndex], this._editor, this._styleClassName, helper, viewZoneAccessor, () => this._detectVisibleLenses && this._detectVisibleLenses.schedule())); + this._lenses.splice(codeLensIndex, 0, new CodeLensWidget(groups[groupsIndex], this._editor, this._styleClassName, helper, viewZoneAccessor, () => this._detectVisibleLenses && this._detectVisibleLenses.schedule())); codeLensIndex++; groupsIndex++; } @@ -314,7 +314,7 @@ export class CodeLensContribution implements editorCommon.IEditorContribution { // Create extra symbols while (groupsIndex < groups.length) { - this._lenses.push(new CodeLensWidget(groups[groupsIndex], this._editor, this._styleClassName, helper, viewZoneAccessor, () => this._detectVisibleLenses && this._detectVisibleLenses.schedule())); + this._lenses.push(new CodeLensWidget(groups[groupsIndex], this._editor, this._styleClassName, helper, viewZoneAccessor, () => this._detectVisibleLenses && this._detectVisibleLenses.schedule())); groupsIndex++; } diff --git a/src/vs/editor/contrib/codelens/codelensWidget.ts b/src/vs/editor/contrib/codelens/codelensWidget.ts index 86e1ec2d68..f98241e5b1 100644 --- a/src/vs/editor/contrib/codelens/codelensWidget.ts +++ b/src/vs/editor/contrib/codelens/codelensWidget.ts @@ -7,7 +7,7 @@ import 'vs/css!./codelensWidget'; import * as dom from 'vs/base/browser/dom'; import { renderCodicons } from 'vs/base/common/codicons'; import { escape } from 'vs/base/common/strings'; -import * as editorBrowser from 'vs/editor/browser/editorBrowser'; +import { IViewZone, IContentWidget, IActiveCodeEditor, IContentWidgetPosition, ContentWidgetPositionPreference, IViewZoneChangeAccessor } from 'vs/editor/browser/editorBrowser'; import { Range } from 'vs/editor/common/core/range'; import { IModelDecorationsChangeAccessor, IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; @@ -17,7 +17,7 @@ import { CodeLensItem } from 'vs/editor/contrib/codelens/codelens'; import { editorActiveLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -class CodeLensViewZone implements editorBrowser.IViewZone { +class CodeLensViewZone implements IViewZone { readonly heightInLines: number; readonly suppressMouseDown: boolean; @@ -47,7 +47,7 @@ class CodeLensViewZone implements editorBrowser.IViewZone { } } -class CodeLensContentWidget implements editorBrowser.IContentWidget { +class CodeLensContentWidget implements IContentWidget { private static _idPool: number = 0; @@ -57,14 +57,14 @@ class CodeLensContentWidget implements editorBrowser.IContentWidget { private readonly _id: string; private readonly _domNode: HTMLElement; - private readonly _editor: editorBrowser.IActiveCodeEditor; + private readonly _editor: IActiveCodeEditor; private readonly _commands = new Map(); - private _widgetPosition?: editorBrowser.IContentWidgetPosition; + private _widgetPosition?: IContentWidgetPosition; private _isEmpty: boolean = true; constructor( - editor: editorBrowser.IActiveCodeEditor, + editor: IActiveCodeEditor, className: string, line: number, ) { @@ -137,11 +137,11 @@ class CodeLensContentWidget implements editorBrowser.IContentWidget { const column = this._editor.getModel().getLineFirstNonWhitespaceColumn(line); this._widgetPosition = { position: { lineNumber: line, column: column }, - preference: [editorBrowser.ContentWidgetPositionPreference.ABOVE] + preference: [ContentWidgetPositionPreference.ABOVE] }; } - getPosition(): editorBrowser.IContentWidgetPosition | null { + getPosition(): IContentWidgetPosition | null { return this._widgetPosition || null; } } @@ -181,7 +181,7 @@ export class CodeLensHelper { export class CodeLensWidget { - private readonly _editor: editorBrowser.IActiveCodeEditor; + private readonly _editor: IActiveCodeEditor; private readonly _className: string; private readonly _viewZone!: CodeLensViewZone; private readonly _viewZoneId!: string; @@ -193,10 +193,10 @@ export class CodeLensWidget { constructor( data: CodeLensItem[], - editor: editorBrowser.IActiveCodeEditor, + editor: IActiveCodeEditor, className: string, helper: CodeLensHelper, - viewZoneChangeAccessor: editorBrowser.IViewZoneChangeAccessor, + viewZoneChangeAccessor: IViewZoneChangeAccessor, updateCallback: Function ) { this._editor = editor; @@ -244,7 +244,7 @@ export class CodeLensWidget { } } - dispose(helper: CodeLensHelper, viewZoneChangeAccessor?: editorBrowser.IViewZoneChangeAccessor): void { + dispose(helper: CodeLensHelper, viewZoneChangeAccessor?: IViewZoneChangeAccessor): void { this._decorationIds.forEach(helper.removeDecoration, helper); this._decorationIds = []; if (viewZoneChangeAccessor) { @@ -322,7 +322,7 @@ export class CodeLensWidget { return -1; } - update(viewZoneChangeAccessor: editorBrowser.IViewZoneChangeAccessor): void { + update(viewZoneChangeAccessor: IViewZoneChangeAccessor): void { if (this.isValid()) { const range = this._editor.getModel().getDecorationRange(this._decorationIds[0]); if (range) { diff --git a/src/vs/editor/contrib/colorPicker/colorPickerWidget.ts b/src/vs/editor/contrib/colorPicker/colorPickerWidget.ts index e8f689e6bf..4af6e98a26 100644 --- a/src/vs/editor/contrib/colorPicker/colorPickerWidget.ts +++ b/src/vs/editor/contrib/colorPicker/colorPickerWidget.ts @@ -163,7 +163,7 @@ class SaturationBox extends Disposable { this.onDidChangePosition(e.offsetX, e.offsetY); } - this.monitor.startMonitoring(standardMouseMoveMerger, event => this.onDidChangePosition(event.posx - origin.left, event.posy - origin.top), () => null); + this.monitor.startMonitoring(e.buttons, standardMouseMoveMerger, event => this.onDidChangePosition(event.posx - origin.left, event.posy - origin.top), () => null); const mouseUpListener = dom.addDisposableGenericMouseUpListner(document, () => { this._onColorFlushed.fire(); @@ -270,7 +270,7 @@ abstract class Strip extends Disposable { this.onDidChangeTop(e.offsetY); } - monitor.startMonitoring(standardMouseMoveMerger, event => this.onDidChangeTop(event.posy - origin.top), () => null); + monitor.startMonitoring(e.buttons, standardMouseMoveMerger, event => this.onDidChangeTop(event.posy - origin.top), () => null); const mouseUpListener = dom.addDisposableGenericMouseUpListner(document, () => { this._onColorFlushed.fire(); diff --git a/src/vs/editor/contrib/comment/blockCommentCommand.ts b/src/vs/editor/contrib/comment/blockCommentCommand.ts index 057d45be6d..36d5edeb48 100644 --- a/src/vs/editor/contrib/comment/blockCommentCommand.ts +++ b/src/vs/editor/contrib/comment/blockCommentCommand.ts @@ -8,11 +8,11 @@ import { EditOperation } from 'vs/editor/common/core/editOperation'; 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 * as editorCommon from 'vs/editor/common/editorCommon'; +import { ICommand, IEditOperationBuilder, ICursorStateComputerData } from 'vs/editor/common/editorCommon'; import { IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; -export class BlockCommentCommand implements editorCommon.ICommand { +export class BlockCommentCommand implements ICommand { private readonly _selection: Selection; private _usedEndToken: string | null; @@ -53,7 +53,7 @@ export class BlockCommentCommand implements editorCommon.ICommand { return true; } - private _createOperationsForBlockComment(selection: Range, startToken: string, endToken: string, model: ITextModel, builder: editorCommon.IEditOperationBuilder): void { + private _createOperationsForBlockComment(selection: Range, startToken: string, endToken: string, model: ITextModel, builder: IEditOperationBuilder): void { const startLineNumber = selection.startLineNumber; const startColumn = selection.startColumn; const endLineNumber = selection.endLineNumber; @@ -164,7 +164,7 @@ export class BlockCommentCommand implements editorCommon.ICommand { return res; } - public getEditOperations(model: ITextModel, builder: editorCommon.IEditOperationBuilder): void { + public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void { const startLineNumber = this._selection.startLineNumber; const startColumn = this._selection.startColumn; @@ -179,7 +179,7 @@ export class BlockCommentCommand implements editorCommon.ICommand { this._createOperationsForBlockComment(this._selection, config.blockCommentStartToken, config.blockCommentEndToken, model, builder); } - public computeCursorState(model: ITextModel, helper: editorCommon.ICursorStateComputerData): Selection { + public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection { const inverseEditOperations = helper.getInverseEditOperations(); if (inverseEditOperations.length === 2) { const startTokenEditOperation = inverseEditOperations[0]; diff --git a/src/vs/editor/contrib/comment/lineCommentCommand.ts b/src/vs/editor/contrib/comment/lineCommentCommand.ts index f8f08ffd3e..78055731bc 100644 --- a/src/vs/editor/contrib/comment/lineCommentCommand.ts +++ b/src/vs/editor/contrib/comment/lineCommentCommand.ts @@ -9,7 +9,7 @@ import { EditOperation } from 'vs/editor/common/core/editOperation'; 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 * as editorCommon from 'vs/editor/common/editorCommon'; +import { ICommand, IEditOperationBuilder, ICursorStateComputerData } from 'vs/editor/common/editorCommon'; import { IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { BlockCommentCommand } from 'vs/editor/contrib/comment/blockCommentCommand'; @@ -47,7 +47,7 @@ export const enum Type { ForceRemove = 2 } -export class LineCommentCommand implements editorCommon.ICommand { +export class LineCommentCommand implements ICommand { private readonly _selection: Selection; private _selectionId: string | null; @@ -187,7 +187,7 @@ export class LineCommentCommand implements editorCommon.ICommand { /** * Given a successful analysis, execute either insert line comments, either remove line comments */ - private _executeLineComments(model: ISimpleModel, builder: editorCommon.IEditOperationBuilder, data: IPreflightDataSupported, s: Selection): void { + private _executeLineComments(model: ISimpleModel, builder: IEditOperationBuilder, data: IPreflightDataSupported, s: Selection): void { let ops: IIdentifiedSingleEditOperation[]; @@ -266,7 +266,7 @@ export class LineCommentCommand implements editorCommon.ICommand { /** * Given an unsuccessful analysis, delegate to the block comment command */ - private _executeBlockComment(model: ITextModel, builder: editorCommon.IEditOperationBuilder, s: Selection): void { + private _executeBlockComment(model: ITextModel, builder: IEditOperationBuilder, s: Selection): void { model.tokenizeIfCheap(s.startLineNumber); let languageId = model.getLanguageIdAtPosition(s.startLineNumber, 1); let config = LanguageConfigurationRegistry.getComments(languageId); @@ -307,7 +307,7 @@ export class LineCommentCommand implements editorCommon.ICommand { } } - public getEditOperations(model: ITextModel, builder: editorCommon.IEditOperationBuilder): void { + public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void { let s = this._selection; this._moveEndPositionDown = false; @@ -325,7 +325,7 @@ export class LineCommentCommand implements editorCommon.ICommand { return this._executeBlockComment(model, builder, s); } - public computeCursorState(model: ITextModel, helper: editorCommon.ICursorStateComputerData): Selection { + public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection { let result = helper.getTrackedSelection(this._selectionId!); if (this._moveEndPositionDown) { @@ -381,7 +381,6 @@ export class LineCommentCommand implements editorCommon.ICommand { return res; } - // TODO@Alex -> duplicated in characterHardWrappingLineMapper private static nextVisibleColumn(currentVisibleColumn: number, tabSize: number, isTab: boolean, columnSize: number): number { if (isTab) { return currentVisibleColumn + (tabSize - (currentVisibleColumn % tabSize)); diff --git a/src/vs/editor/contrib/dnd/dnd.ts b/src/vs/editor/contrib/dnd/dnd.ts index c8c2d20ba6..7e86fa07bc 100644 --- a/src/vs/editor/contrib/dnd/dnd.ts +++ b/src/vs/editor/contrib/dnd/dnd.ts @@ -10,7 +10,7 @@ import { isMacintosh } from 'vs/base/common/platform'; import { KeyCode } from 'vs/base/common/keyCodes'; import { ICodeEditor, IEditorMouseEvent, IMouseTarget, MouseTargetType, IPartialEditorMouseEvent } from 'vs/editor/browser/editorBrowser'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import * as editorCommon from 'vs/editor/common/editorCommon'; +import { IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; @@ -29,7 +29,7 @@ function hasTriggerModifier(e: IKeyboardEvent | IMouseEvent): boolean { } } -export class DragAndDropController extends Disposable implements editorCommon.IEditorContribution { +export class DragAndDropController extends Disposable implements IEditorContribution { public static readonly ID = 'editor.contrib.dragAndDrop'; @@ -201,7 +201,7 @@ export class DragAndDropController extends Disposable implements editorCommon.IE }]; this._dndDecorationIds = this._editor.deltaDecorations(this._dndDecorationIds, newDecorations); - this._editor.revealPosition(position, editorCommon.ScrollType.Immediate); + this._editor.revealPosition(position, ScrollType.Immediate); } private _removeDecoration(): void { diff --git a/src/vs/editor/contrib/dnd/dragAndDropCommand.ts b/src/vs/editor/contrib/dnd/dragAndDropCommand.ts index cbb8da1531..87d8e91521 100644 --- a/src/vs/editor/contrib/dnd/dragAndDropCommand.ts +++ b/src/vs/editor/contrib/dnd/dragAndDropCommand.ts @@ -3,14 +3,14 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as editorCommon from 'vs/editor/common/editorCommon'; +import { ICommand, IEditOperationBuilder, ICursorStateComputerData } from 'vs/editor/common/editorCommon'; import { Selection } from 'vs/editor/common/core/selection'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; -export class DragAndDropCommand implements editorCommon.ICommand { +export class DragAndDropCommand implements ICommand { private readonly selection: Selection; private readonly targetPosition: Position; @@ -24,7 +24,7 @@ export class DragAndDropCommand implements editorCommon.ICommand { this.targetSelection = null; } - public getEditOperations(model: ITextModel, builder: editorCommon.IEditOperationBuilder): void { + public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void { let text = model.getValueInRange(this.selection); if (!this.copy) { builder.addEditOperation(this.selection, null); @@ -102,7 +102,7 @@ export class DragAndDropCommand implements editorCommon.ICommand { } } - public computeCursorState(model: ITextModel, helper: editorCommon.ICursorStateComputerData): Selection { + public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection { return this.targetSelection!; } } diff --git a/src/vs/editor/contrib/find/findController.ts b/src/vs/editor/contrib/find/findController.ts index c57a8e426c..94aa4e5776 100644 --- a/src/vs/editor/contrib/find/findController.ts +++ b/src/vs/editor/contrib/find/findController.ts @@ -10,7 +10,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, EditorCommand, ServicesAccessor, registerEditorAction, registerEditorCommand, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import * as editorCommon from 'vs/editor/common/editorCommon'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { CONTEXT_FIND_INPUT_FOCUSED, CONTEXT_FIND_WIDGET_VISIBLE, FIND_IDS, FindModelBoundToEditorModel, ToggleCaseSensitiveKeybinding, ToggleRegexKeybinding, ToggleSearchScopeKeybinding, ToggleWholeWordKeybinding, CONTEXT_REPLACE_INPUT_FOCUSED } from 'vs/editor/contrib/find/findModel'; import { FindOptionsWidget } from 'vs/editor/contrib/find/findOptionsWidget'; @@ -68,7 +68,7 @@ export interface IFindStartOptions { updateSearchScope: boolean; } -export class CommonFindController extends Disposable implements editorCommon.IEditorContribution { +export class CommonFindController extends Disposable implements IEditorContribution { public static readonly ID = 'editor.contrib.findController'; diff --git a/src/vs/editor/contrib/find/findModel.ts b/src/vs/editor/contrib/find/findModel.ts index 8c76b84fcb..8f09657907 100644 --- a/src/vs/editor/contrib/find/findModel.ts +++ b/src/vs/editor/contrib/find/findModel.ts @@ -13,7 +13,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 { Constants } from 'vs/base/common/uint'; -import * as editorCommon from 'vs/editor/common/editorCommon'; +import { ScrollType, ICommand } from 'vs/editor/common/editorCommon'; import { EndOfLinePreference, FindMatch, ITextModel } from 'vs/editor/common/model'; import { SearchParams } from 'vs/editor/common/model/textModelSearch'; import { FindDecorations } from 'vs/editor/contrib/find/findDecorations'; @@ -209,7 +209,7 @@ export class FindModelBoundToEditorModel { let findScope = this._decorations.getFindScope(); if (findScope) { // Reveal the selection so user is reminded that 'selection find' is on. - this._editor.revealRangeInCenterIfOutsideViewport(findScope, editorCommon.ScrollType.Smooth); + this._editor.revealRangeInCenterIfOutsideViewport(findScope, ScrollType.Smooth); } return true; } @@ -225,7 +225,7 @@ export class FindModelBoundToEditorModel { ); this._editor.setSelection(match); - this._editor.revealRangeInCenterIfOutsideViewport(match, editorCommon.ScrollType.Smooth); + this._editor.revealRangeInCenterIfOutsideViewport(match, ScrollType.Smooth); } private _prevSearchPosition(before: Position) { @@ -536,7 +536,7 @@ export class FindModelBoundToEditorModel { this._editor.setSelections(selections); } - private _executeEditorCommand(source: string, command: editorCommon.ICommand): void { + private _executeEditorCommand(source: string, command: ICommand): void { try { this._ignoreModelContentChanged = true; this._editor.pushUndoStop(); diff --git a/src/vs/editor/contrib/find/findWidget.ts b/src/vs/editor/contrib/find/findWidget.ts index 6c551f2fab..d8537c008f 100644 --- a/src/vs/editor/contrib/find/findWidget.ts +++ b/src/vs/editor/contrib/find/findWidget.ts @@ -908,7 +908,6 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas return null; } try { - /* tslint:disable-next-line:no-unused-expression */ new RegExp(value); return null; } catch (e) { diff --git a/src/vs/editor/contrib/find/replaceAllCommand.ts b/src/vs/editor/contrib/find/replaceAllCommand.ts index ec8bc4dca4..1d8291e522 100644 --- a/src/vs/editor/contrib/find/replaceAllCommand.ts +++ b/src/vs/editor/contrib/find/replaceAllCommand.ts @@ -5,7 +5,7 @@ import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; -import * as editorCommon from 'vs/editor/common/editorCommon'; +import { ICommand, IEditOperationBuilder, ICursorStateComputerData } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; interface IEditOperation { @@ -13,7 +13,7 @@ interface IEditOperation { text: string; } -export class ReplaceAllCommand implements editorCommon.ICommand { +export class ReplaceAllCommand implements ICommand { private readonly _editorSelection: Selection; private _trackedEditorSelectionId: string | null; @@ -27,7 +27,7 @@ export class ReplaceAllCommand implements editorCommon.ICommand { this._trackedEditorSelectionId = null; } - public getEditOperations(model: ITextModel, builder: editorCommon.IEditOperationBuilder): void { + public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void { if (this._ranges.length > 0) { // Collect all edit operations let ops: IEditOperation[] = []; @@ -66,7 +66,7 @@ export class ReplaceAllCommand implements editorCommon.ICommand { this._trackedEditorSelectionId = builder.trackSelection(this._editorSelection); } - public computeCursorState(model: ITextModel, helper: editorCommon.ICursorStateComputerData): Selection { + public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection { return helper.getTrackedSelection(this._trackedEditorSelectionId!); } } diff --git a/src/vs/editor/contrib/folding/folding.ts b/src/vs/editor/contrib/folding/folding.ts index 3eb92d6220..8db8050da7 100644 --- a/src/vs/editor/contrib/folding/folding.ts +++ b/src/vs/editor/contrib/folding/folding.ts @@ -14,12 +14,12 @@ import { ScrollType, IEditorContribution } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; import { registerEditorAction, registerEditorContribution, ServicesAccessor, EditorAction, registerInstantiatedEditorAction } from 'vs/editor/browser/editorExtensions'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; -import { FoldingModel, setCollapseStateAtLevel, CollapseMemento, setCollapseStateLevelsDown, setCollapseStateLevelsUp, setCollapseStateForMatchingLines, setCollapseStateForType, toggleCollapseState } from 'vs/editor/contrib/folding/foldingModel'; +import { FoldingModel, setCollapseStateAtLevel, CollapseMemento, setCollapseStateLevelsDown, setCollapseStateLevelsUp, setCollapseStateForMatchingLines, setCollapseStateForType, toggleCollapseState, setCollapseStateUp } from 'vs/editor/contrib/folding/foldingModel'; import { FoldingDecorationProvider } from './foldingDecorations'; import { FoldingRegions, FoldingRegion } from './foldingRanges'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; -import { IMarginData, IEmptyContentData } from 'vs/editor/browser/controller/mouseTarget'; +import { IMarginData } from 'vs/editor/browser/controller/mouseTarget'; import { HiddenRangeModel } from 'vs/editor/contrib/folding/hiddenRangeModel'; import { IRange } from 'vs/editor/common/core/range'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; @@ -32,6 +32,8 @@ import { InitializingRangeProvider, ID_INIT_PROVIDER } from 'vs/editor/contrib/f import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { onUnexpectedError } from 'vs/base/common/errors'; import { RawContextKey, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { registerColor, editorSelectionBackground, transparent } from 'vs/platform/theme/common/colorRegistry'; const CONTEXT_FOLDING_ENABLED = new RawContextKey('foldingEnabled', false); @@ -59,7 +61,6 @@ export class FoldingController extends Disposable implements IEditorContribution private readonly editor: ICodeEditor; private _isEnabled: boolean; - private _autoHideFoldingControls: boolean; private _useFoldingProviders: boolean; private readonly foldingDecorationProvider: FoldingDecorationProvider; @@ -89,7 +90,6 @@ export class FoldingController extends Disposable implements IEditorContribution this.editor = editor; const options = this.editor.getOptions(); this._isEnabled = options.get(EditorOption.folding); - this._autoHideFoldingControls = options.get(EditorOption.showFoldingControls) === 'mouseover'; this._useFoldingProviders = options.get(EditorOption.foldingStrategy) !== 'indentation'; this.foldingModel = null; @@ -103,32 +103,30 @@ export class FoldingController extends Disposable implements IEditorContribution this.mouseDownInfo = null; this.foldingDecorationProvider = new FoldingDecorationProvider(editor); - this.foldingDecorationProvider.autoHideFoldingControls = this._autoHideFoldingControls; + this.foldingDecorationProvider.autoHideFoldingControls = options.get(EditorOption.showFoldingControls) === 'mouseover'; + this.foldingDecorationProvider.showFoldingHighlights = options.get(EditorOption.foldingHighlight); this.foldingEnabled = CONTEXT_FOLDING_ENABLED.bindTo(this.contextKeyService); this.foldingEnabled.set(this._isEnabled); this._register(this.editor.onDidChangeModel(() => this.onModelChanged())); this._register(this.editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { - if (e.hasChanged(EditorOption.folding) || e.hasChanged(EditorOption.showFoldingControls) || e.hasChanged(EditorOption.foldingStrategy)) { - let oldIsEnabled = this._isEnabled; + if (e.hasChanged(EditorOption.folding)) { const options = this.editor.getOptions(); this._isEnabled = options.get(EditorOption.folding); this.foldingEnabled.set(this._isEnabled); - if (oldIsEnabled !== this._isEnabled) { - this.onModelChanged(); - } - let oldShowFoldingControls = this._autoHideFoldingControls; - this._autoHideFoldingControls = options.get(EditorOption.showFoldingControls) === 'mouseover'; - if (oldShowFoldingControls !== this._autoHideFoldingControls) { - this.foldingDecorationProvider.autoHideFoldingControls = this._autoHideFoldingControls; - this.onModelContentChanged(); - } - let oldUseFoldingProviders = this._useFoldingProviders; + this.onModelChanged(); + } + if (e.hasChanged(EditorOption.showFoldingControls) || e.hasChanged(EditorOption.foldingHighlight)) { + const options = this.editor.getOptions(); + this.foldingDecorationProvider.autoHideFoldingControls = options.get(EditorOption.showFoldingControls) === 'mouseover'; + this.foldingDecorationProvider.showFoldingHighlights = options.get(EditorOption.foldingHighlight); + this.onModelContentChanged(); + } + if (e.hasChanged(EditorOption.foldingStrategy)) { + const options = this.editor.getOptions(); this._useFoldingProviders = options.get(EditorOption.foldingStrategy) !== 'indentation'; - if (oldUseFoldingProviders !== this._useFoldingProviders) { - this.onFoldingStrategyChanged(); - } + this.onFoldingStrategyChanged(); } })); this.onModelChanged(); @@ -366,15 +364,6 @@ export class FoldingController extends Disposable implements IEditorContribution iconClicked = true; break; - case MouseTargetType.CONTENT_EMPTY: { - if (this.hiddenRangeModel.hasRanges()) { - const data = e.target.detail as IEmptyContentData; - if (!data.isAfterLines) { - break; - } - } - return; - } case MouseTargetType.CONTENT_TEXT: { if (this.hiddenRangeModel.hasRanges()) { let model = this.editor.getModel(); @@ -619,9 +608,10 @@ class FoldAction extends FoldingAction { { name: 'Fold editor argument', description: `Property-value pairs that can be passed through this argument: - * 'levels': Number of levels to fold. Defaults to 1. + * 'levels': Number of levels to fold. * 'direction': If 'up', folds given number of levels up otherwise folds down. * 'selectionLines': The start lines (0-based) of the editor selections to apply the fold action to. If not set, the active selection(s) will be used. + If no levels or direction is set, folds the region at the locations or if already collapsed, the first uncollapsed parent instead. `, constraint: foldingArgumentsConstraint, schema: { @@ -629,12 +619,10 @@ class FoldAction extends FoldingAction { 'properties': { 'levels': { 'type': 'number', - 'default': 1 }, 'direction': { 'type': 'string', 'enum': ['up', 'down'], - 'default': 'down' }, 'selectionLines': { 'type': 'array', @@ -651,12 +639,20 @@ class FoldAction extends FoldingAction { } invoke(_foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor, args: FoldingArguments): void { - let levels = args && args.levels || 1; let lineNumbers = this.getLineNumbers(args, editor); - if (args && args.direction === 'up') { - setCollapseStateLevelsUp(foldingModel, true, levels, lineNumbers); + + const levels = args && args.levels; + const direction = args && args.direction; + + if (typeof levels !== 'number' && typeof direction !== 'string') { + // fold the region at the location or if already collapsed, the first uncollapsed parent instead. + setCollapseStateUp(foldingModel, true, lineNumbers); } else { - setCollapseStateLevelsDown(foldingModel, true, levels, lineNumbers); + if (direction === 'up') { + setCollapseStateLevelsUp(foldingModel, true, levels || 1, lineNumbers); + } else { + setCollapseStateLevelsDown(foldingModel, true, levels || 1, lineNumbers); + } } } } @@ -888,3 +884,12 @@ for (let i = 1; i <= 7; i++) { }) ); } + +export const foldBackgroundBackground = registerColor('editor.foldBackground', { light: transparent(editorSelectionBackground, 0.3), dark: transparent(editorSelectionBackground, 0.3), hc: null }, nls.localize('editorSelectionBackground', "Color of the editor selection.")); + +registerThemingParticipant((theme, collector) => { + const foldBackground = theme.getColor(foldBackgroundBackground); + if (foldBackground) { + collector.addRule(`.monaco-editor .folded-background { background-color: ${foldBackground}; }`); + } +}); diff --git a/src/vs/editor/contrib/folding/foldingDecorations.ts b/src/vs/editor/contrib/folding/foldingDecorations.ts index 0ac4856cbb..28224b35c2 100644 --- a/src/vs/editor/contrib/folding/foldingDecorations.ts +++ b/src/vs/editor/contrib/folding/foldingDecorations.ts @@ -11,6 +11,12 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; export class FoldingDecorationProvider implements IDecorationProvider { private static readonly COLLAPSED_VISUAL_DECORATION = ModelDecorationOptions.register({ + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + afterContentClassName: 'inline-folded', + linesDecorationsClassName: 'codicon codicon-chevron-right' + }); + + private static readonly COLLAPSED_HIGHLIGHTED_VISUAL_DECORATION = ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, afterContentClassName: 'inline-folded', className: 'folded-background', @@ -30,12 +36,14 @@ export class FoldingDecorationProvider implements IDecorationProvider { public autoHideFoldingControls: boolean = true; + public showFoldingHighlights: boolean = true; + constructor(private readonly editor: ICodeEditor) { } getDecorationOption(isCollapsed: boolean): ModelDecorationOptions { if (isCollapsed) { - return FoldingDecorationProvider.COLLAPSED_VISUAL_DECORATION; + return this.showFoldingHighlights ? FoldingDecorationProvider.COLLAPSED_HIGHLIGHTED_VISUAL_DECORATION : FoldingDecorationProvider.COLLAPSED_VISUAL_DECORATION; } else if (this.autoHideFoldingControls) { return FoldingDecorationProvider.EXPANDED_AUTO_HIDE_VISUAL_DECORATION; } else { diff --git a/src/vs/editor/contrib/folding/foldingModel.ts b/src/vs/editor/contrib/folding/foldingModel.ts index 6c019eff4d..74e9b4cea3 100644 --- a/src/vs/editor/contrib/folding/foldingModel.ts +++ b/src/vs/editor/contrib/folding/foldingModel.ts @@ -6,9 +6,6 @@ import { ITextModel, IModelDecorationOptions, IModelDeltaDecoration, IModelDecorationsChangeAccessor } from 'vs/editor/common/model'; import { Event, Emitter } from 'vs/base/common/event'; import { FoldingRegions, ILineRange, FoldingRegion } from './foldingRanges'; -import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { registerColor, editorSelectionBackground, darken, lighten } from 'vs/platform/theme/common/colorRegistry'; -import * as nls from 'vs/nls'; export interface IDecorationProvider { getDecorationOption(isCollapsed: boolean): IModelDecorationOptions; @@ -276,7 +273,7 @@ export function toggleCollapseState(foldingModel: FoldingModel, levels: number, * @param levels The number of levels. Use 1 to only impact the regions at the location, use Number.MAX_VALUE for all levels. * @param lineNumbers the location of the regions to collapse or expand, or if not set, all regions in the model. */ -export function setCollapseStateLevelsDown(foldingModel: FoldingModel, doCollapse: boolean, levels = Number.MAX_VALUE, lineNumbers?: number[]) { +export function setCollapseStateLevelsDown(foldingModel: FoldingModel, doCollapse: boolean, levels = Number.MAX_VALUE, lineNumbers?: number[]): void { let toToggle: FoldingRegion[] = []; if (lineNumbers && lineNumbers.length > 0) { for (let lineNumber of lineNumbers) { @@ -302,9 +299,9 @@ export function setCollapseStateLevelsDown(foldingModel: FoldingModel, doCollaps * Collapse or expand the regions at the given locations including all parents. * @param doCollapse Wheter to collase or expand * @param levels The number of levels. Use 1 to only impact the regions at the location, use Number.MAX_VALUE for all levels. - * @param lineNumbers the location of the regions to collapse or expand, or if not set, all regions in the model. + * @param lineNumbers the location of the regions to collapse or expand. */ -export function setCollapseStateLevelsUp(foldingModel: FoldingModel, doCollapse: boolean, levels: number, lineNumbers: number[]) { +export function setCollapseStateLevelsUp(foldingModel: FoldingModel, doCollapse: boolean, levels: number, lineNumbers: number[]): void { let toToggle: FoldingRegion[] = []; for (let lineNumber of lineNumbers) { let regions = foldingModel.getAllRegionsAtLine(lineNumber, (region, level) => region.isCollapsed !== doCollapse && level <= levels); @@ -313,6 +310,22 @@ export function setCollapseStateLevelsUp(foldingModel: FoldingModel, doCollapse: foldingModel.toggleCollapseState(toToggle); } +/** + * Collapse or expand a region at the given locations. If the inner most region is already collapsed/expanded, uses the first parent instead. + * @param doCollapse Wheter to collase or expand + * @param lineNumbers the location of the regions to collapse or expand. + */ +export function setCollapseStateUp(foldingModel: FoldingModel, doCollapse: boolean, lineNumbers: number[]): void { + let toToggle: FoldingRegion[] = []; + for (let lineNumber of lineNumbers) { + let regions = foldingModel.getAllRegionsAtLine(lineNumber, (region, ) => region.isCollapsed !== doCollapse); + if (regions.length > 0) { + toToggle.push(regions[0]); + } + } + foldingModel.toggleCollapseState(toToggle); +} + /** * Folds or unfolds all regions that have a given level, except if they contain one of the blocked lines. * @param foldLevel level. Level == 1 is the top level @@ -357,12 +370,3 @@ export function setCollapseStateForType(foldingModel: FoldingModel, type: string } foldingModel.toggleCollapseState(toToggle); } - -export const foldBackgroundBackground = registerColor('editor.foldBackground', { light: lighten(editorSelectionBackground, 0.5), dark: darken(editorSelectionBackground, 0.5), hc: null }, nls.localize('editorSelectionBackground', "Color of the editor selection.")); - -registerThemingParticipant((theme, collector) => { - const foldBackground = theme.getColor(foldBackgroundBackground); - if (foldBackground) { - collector.addRule(`.monaco-editor .folded-background { background-color: ${foldBackground}; }`); - } -}); diff --git a/src/vs/editor/contrib/folding/indentRangeProvider.ts b/src/vs/editor/contrib/folding/indentRangeProvider.ts index 6f2d080847..e9a7e3d60a 100644 --- a/src/vs/editor/contrib/folding/indentRangeProvider.ts +++ b/src/vs/editor/contrib/folding/indentRangeProvider.ts @@ -66,7 +66,7 @@ export class RangesCollector { // reverse and create arrays of the exact length let startIndexes = new Uint32Array(this._length); let endIndexes = new Uint32Array(this._length); - for (let i = this._length - 1, k = 0; i >= 0; i-- , k++) { + for (let i = this._length - 1, k = 0; i >= 0; i--, k++) { startIndexes[k] = this._startIndexes[i]; endIndexes[k] = this._endIndexes[i]; } diff --git a/src/vs/editor/contrib/folding/test/foldingModel.test.ts b/src/vs/editor/contrib/folding/test/foldingModel.test.ts index d87acd7355..cfcaf71125 100644 --- a/src/vs/editor/contrib/folding/test/foldingModel.test.ts +++ b/src/vs/editor/contrib/folding/test/foldingModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { FoldingModel, setCollapseStateAtLevel, setCollapseStateLevelsDown, setCollapseStateLevelsUp, setCollapseStateForMatchingLines } from 'vs/editor/contrib/folding/foldingModel'; +import { FoldingModel, setCollapseStateAtLevel, setCollapseStateLevelsDown, setCollapseStateLevelsUp, setCollapseStateForMatchingLines, setCollapseStateUp } from 'vs/editor/contrib/folding/foldingModel'; import { TextModel, ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { computeRanges } from 'vs/editor/contrib/folding/indentRangeProvider'; import { TrackedRangeStickiness, IModelDeltaDecoration, ITextModel, IModelDecorationsChangeAccessor } from 'vs/editor/common/model'; @@ -587,6 +587,50 @@ suite('Folding Model', () => { }); + test('setCollapseStateUp', () => { + let lines = [ + /* 1*/ '//#region', + /* 2*/ '//#endregion', + /* 3*/ 'class A {', + /* 4*/ ' void foo() {', + /* 5*/ ' if (true) {', + /* 6*/ ' return;', + /* 7*/ ' }', + /* 8*/ '', + /* 9*/ ' if (true) {', + /* 10*/ ' return;', + /* 11*/ ' }', + /* 12*/ ' }', + /* 13*/ '}']; + + let textModel = TextModel.createFromString(lines.join('\n')); + try { + let foldingModel = new FoldingModel(textModel, new TestDecorationProvider(textModel)); + + let ranges = computeRanges(textModel, false, { start: /^\/\/#region$/, end: /^\/\/#endregion$/ }); + foldingModel.update(ranges); + + let r1 = r(1, 2, false); + let r2 = r(3, 12, false); + let r3 = r(4, 11, false); + let r4 = r(5, 6, false); + let r5 = r(9, 10, false); + assertRanges(foldingModel, [r1, r2, r3, r4, r5]); + + setCollapseStateUp(foldingModel, true, [5]); + assertFoldedRanges(foldingModel, [r4], '1'); + + setCollapseStateUp(foldingModel, true, [5]); + assertFoldedRanges(foldingModel, [r3, r4], '2'); + + setCollapseStateUp(foldingModel, true, [4]); + assertFoldedRanges(foldingModel, [r2, r3, r4], '2'); + } finally { + textModel.dispose(); + } + + }); + test('setCollapseStateForMatchingLines', () => { let lines = [ diff --git a/src/vs/editor/contrib/format/format.ts b/src/vs/editor/contrib/format/format.ts index 3d06bd3b0a..8baeacfae4 100644 --- a/src/vs/editor/contrib/format/format.ts +++ b/src/vs/editor/contrib/format/format.ts @@ -14,7 +14,7 @@ import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; 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 * as editorCommon from 'vs/editor/common/editorCommon'; +import { ScrollType } from 'vs/editor/common/editorCommon'; import { ISingleEditOperation, ITextModel } from 'vs/editor/common/model'; import { DocumentFormattingEditProvider, DocumentFormattingEditProviderRegistry, DocumentRangeFormattingEditProvider, DocumentRangeFormattingEditProviderRegistry, FormattingOptions, OnTypeFormattingEditProviderRegistry, TextEdit } from 'vs/editor/common/modes'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; @@ -182,7 +182,7 @@ export async function formatDocumentRangeWithProvider( alertFormattingEdits(edits); editorOrModel.pushUndoStop(); editorOrModel.focus(); - editorOrModel.revealPositionInCenterIfOutsideViewport(editorOrModel.getPosition(), editorCommon.ScrollType.Immediate); + editorOrModel.revealPositionInCenterIfOutsideViewport(editorOrModel.getPosition(), ScrollType.Immediate); } else { // use model to apply edits @@ -272,7 +272,7 @@ export async function formatDocumentWithProvider( alertFormattingEdits(edits); editorOrModel.pushUndoStop(); editorOrModel.focus(); - editorOrModel.revealPositionInCenterIfOutsideViewport(editorOrModel.getPosition(), editorCommon.ScrollType.Immediate); + editorOrModel.revealPositionInCenterIfOutsideViewport(editorOrModel.getPosition(), ScrollType.Immediate); } } else { diff --git a/src/vs/editor/contrib/format/formatActions.ts b/src/vs/editor/contrib/format/formatActions.ts index 6851ebeefa..8dc2a18262 100644 --- a/src/vs/editor/contrib/format/formatActions.ts +++ b/src/vs/editor/contrib/format/formatActions.ts @@ -12,7 +12,7 @@ import { EditorAction, registerEditorAction, registerEditorContribution, Service import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { CharacterSet } from 'vs/editor/common/core/characterClassifier'; import { Range } from 'vs/editor/common/core/range'; -import * as editorCommon from 'vs/editor/common/editorCommon'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { DocumentRangeFormattingEditProviderRegistry, OnTypeFormattingEditProviderRegistry } from 'vs/editor/common/modes'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; @@ -26,7 +26,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { onUnexpectedError } from 'vs/base/common/errors'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; -class FormatOnType implements editorCommon.IEditorContribution { +class FormatOnType implements IEditorContribution { public static readonly ID = 'editor.contrib.autoFormat'; @@ -149,7 +149,7 @@ class FormatOnType implements editorCommon.IEditorContribution { } } -class FormatOnPaste implements editorCommon.IEditorContribution { +class FormatOnPaste implements IEditorContribution { public static readonly ID = 'editor.contrib.formatOnPaste'; diff --git a/src/vs/editor/contrib/gotoError/gotoError.ts b/src/vs/editor/contrib/gotoError/gotoError.ts index 60c3621d37..8786f8400e 100644 --- a/src/vs/editor/contrib/gotoError/gotoError.ts +++ b/src/vs/editor/contrib/gotoError/gotoError.ts @@ -12,7 +12,7 @@ import { RawContextKey, IContextKey, IContextKeyService } from 'vs/platform/cont import { IMarker, IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import * as editorCommon from 'vs/editor/common/editorCommon'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { registerEditorAction, registerEditorContribution, ServicesAccessor, IActionOptions, EditorAction, EditorCommand, registerEditorCommand } from 'vs/editor/browser/editorExtensions'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -189,7 +189,7 @@ class MarkerModel { } } -export class MarkerController implements editorCommon.IEditorContribution { +export class MarkerController implements IEditorContribution { public static readonly ID = 'editor.contrib.markerController'; @@ -240,8 +240,8 @@ export class MarkerController implements editorCommon.IEditorContribution { const prevMarkerKeybinding = this._keybindingService.lookupKeybinding(PrevMarkerAction.ID); const nextMarkerKeybinding = this._keybindingService.lookupKeybinding(NextMarkerAction.ID); const actions = [ - new Action(PrevMarkerAction.ID, PrevMarkerAction.LABEL + (prevMarkerKeybinding ? ` (${prevMarkerKeybinding.getLabel()})` : ''), 'show-previous-problem codicon-chevron-up', this._model.canNavigate(), async () => { if (this._model) { this._model.move(false, true); } }), - new Action(NextMarkerAction.ID, NextMarkerAction.LABEL + (nextMarkerKeybinding ? ` (${nextMarkerKeybinding.getLabel()})` : ''), 'show-next-problem codicon-chevron-down', this._model.canNavigate(), async () => { if (this._model) { this._model.move(true, true); } }) + new Action(NextMarkerAction.ID, NextMarkerAction.LABEL + (nextMarkerKeybinding ? ` (${nextMarkerKeybinding.getLabel()})` : ''), 'show-next-problem codicon-chevron-down', this._model.canNavigate(), async () => { if (this._model) { this._model.move(true, true); } }), + new Action(PrevMarkerAction.ID, PrevMarkerAction.LABEL + (prevMarkerKeybinding ? ` (${prevMarkerKeybinding.getLabel()})` : ''), 'show-previous-problem codicon-chevron-up', this._model.canNavigate(), async () => { if (this._model) { this._model.move(false, true); } }) ]; this._widget = new MarkerNavigationWidget(this._editor, actions, this._themeService); this._widgetVisible.set(true); @@ -424,7 +424,7 @@ export class NextMarkerAction extends MarkerNavigationAction { label: NextMarkerAction.LABEL, alias: 'Go to Next Problem (Error, Warning, Info)', precondition: EditorContextKeys.writable, - kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Alt | KeyCode.F8, weight: KeybindingWeight.EditorContrib } + kbOpts: { kbExpr: EditorContextKeys.focus, primary: KeyMod.Alt | KeyCode.F8, weight: KeybindingWeight.EditorContrib } }); } } @@ -438,7 +438,7 @@ class PrevMarkerAction extends MarkerNavigationAction { label: PrevMarkerAction.LABEL, alias: 'Go to Previous Problem (Error, Warning, Info)', precondition: EditorContextKeys.writable, - kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyMod.Shift | KeyMod.Alt | KeyCode.F8, weight: KeybindingWeight.EditorContrib } + kbOpts: { kbExpr: EditorContextKeys.focus, primary: KeyMod.Shift | KeyMod.Alt | KeyCode.F8, weight: KeybindingWeight.EditorContrib } }); } } diff --git a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts index f71872e33a..b33bfe4720 100644 --- a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts +++ b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts @@ -228,7 +228,7 @@ export class MarkerNavigationWidget extends PeekViewWidget { protected _fillHead(container: HTMLElement): void { super._fillHead(container); - this._actionbarWidget!.push(this.actions, { label: false, icon: true }); + this._actionbarWidget!.push(this.actions, { label: false, icon: true, index: 0 }); } protected _fillTitleIcon(container: HTMLElement): void { @@ -237,7 +237,7 @@ export class MarkerNavigationWidget extends PeekViewWidget { protected _getActionBarOptions(): IActionBarOptions { return { - orientation: ActionsOrientation.HORIZONTAL_REVERSE + orientation: ActionsOrientation.HORIZONTAL }; } diff --git a/src/vs/editor/contrib/gotoSymbol/goToCommands.ts b/src/vs/editor/contrib/gotoSymbol/goToCommands.ts index f25cdaf89f..e129d659e5 100644 --- a/src/vs/editor/contrib/gotoSymbol/goToCommands.ts +++ b/src/vs/editor/contrib/gotoSymbol/goToCommands.ts @@ -27,7 +27,7 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { INotificationService } from 'vs/platform/notification/common/notification'; import { IEditorProgressService } from 'vs/platform/progress/common/progress'; import { getDefinitionsAtPosition, getImplementationsAtPosition, getTypeDefinitionsAtPosition, getDeclarationsAtPosition, getReferencesAtPosition } from './goToSymbol'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { EditorStateCancellationTokenSource, CodeEditorStateFlag } from 'vs/editor/browser/core/editorState'; import { ISymbolNavigationService } from 'vs/editor/contrib/gotoSymbol/symbolNavigation'; import { EditorOption, GoToLocationValues } from 'vs/editor/common/config/editorOptions'; @@ -681,14 +681,11 @@ registerEditorAction(class PeekReferencesAction extends ReferencesAction { class GenericGoToLocationAction extends SymbolNavigationAction { constructor( + config: SymbolNavigationActionConfig, private readonly _references: Location[], - private readonly _gotoMultipleBehaviour: GoToLocationValues | undefined + private readonly _gotoMultipleBehaviour: GoToLocationValues | undefined, ) { - super({ - muteMessage: true, - openInPeek: false, - openToSide: false - }, { + super(config, { id: 'editor.action.goToLocation', label: nls.localize('label.generic', "Go To Any Symbol"), alias: 'Go To Any Symbol', @@ -725,11 +722,12 @@ CommandsRegistry.registerCommand({ { name: 'multiple', description: 'Define what to do when having multiple results, either `peek`, `gotoAndPeek`, or `goto' }, ] }, - handler: async (accessor: ServicesAccessor, resource: any, position: any, references: any, multiple?: any) => { + handler: async (accessor: ServicesAccessor, resource: any, position: any, references: any, multiple?: any, openInPeek?: boolean) => { assertType(URI.isUri(resource)); assertType(corePosition.Position.isIPosition(position)); assertType(Array.isArray(references)); assertType(typeof multiple === 'undefined' || typeof multiple === 'string'); + assertType(typeof openInPeek === 'undefined' || typeof openInPeek === 'boolean'); const editorService = accessor.get(ICodeEditorService); const editor = await editorService.openCodeEditor({ resource }, editorService.getFocusedCodeEditor()); @@ -739,13 +737,29 @@ CommandsRegistry.registerCommand({ editor.revealPositionInCenterIfOutsideViewport(position, ScrollType.Smooth); return editor.invokeWithinContext(accessor => { - const command = new GenericGoToLocationAction(references, multiple as GoToLocationValues); + const command = new GenericGoToLocationAction({ muteMessage: true, openInPeek: Boolean(openInPeek), openToSide: false }, references, multiple as GoToLocationValues); accessor.get(IInstantiationService).invokeFunction(command.run.bind(command), editor); }); } } }); +CommandsRegistry.registerCommand({ + id: 'editor.action.peekLocations', + description: { + description: 'Peek locations from a position in a file', + args: [ + { name: 'uri', description: 'The text document in which to start', constraint: URI }, + { name: 'position', description: 'The position at which to start', constraint: corePosition.Position.isIPosition }, + { name: 'locations', description: 'An array of locations.', constraint: Array }, + { name: 'multiple', description: 'Define what to do when having multiple results, either `peek`, `gotoAndPeek`, or `goto' }, + ] + }, + handler: async (accessor: ServicesAccessor, resource: any, position: any, references: any, multiple?: any) => { + accessor.get(ICommandService).executeCommand('editor.action.goToLocations', resource, position, references, multiple, true); + } +}); + //#endregion @@ -776,6 +790,6 @@ CommandsRegistry.registerCommand({ }); // use NEW command -CommandsRegistry.registerCommandAlias('editor.action.showReferences', 'editor.action.goToLocations'); +CommandsRegistry.registerCommandAlias('editor.action.showReferences', 'editor.action.peekLocations'); //#endregion diff --git a/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts b/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts index 69160c41c2..24629d038a 100644 --- a/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts +++ b/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts @@ -11,7 +11,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { IModeService } from 'vs/editor/common/services/modeService'; import { Range, IRange } from 'vs/editor/common/core/range'; -import * as editorCommon from 'vs/editor/common/editorCommon'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { DefinitionProviderRegistry, LocationLink } from 'vs/editor/common/modes'; import { ICodeEditor, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; @@ -28,7 +28,7 @@ import { Position } from 'vs/editor/common/core/position'; import { withNullAsUndefined } from 'vs/base/common/types'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -export class GotoDefinitionAtPositionEditorContribution implements editorCommon.IEditorContribution { +export class GotoDefinitionAtPositionEditorContribution implements IEditorContribution { public static readonly ID = 'editor.contrib.gotodefinitionatposition'; static readonly MAX_SOURCE_PREVIEW_LINES = 8; diff --git a/src/vs/editor/contrib/gotoSymbol/peek/referencesController.ts b/src/vs/editor/contrib/gotoSymbol/peek/referencesController.ts index 28c3271207..a5a30a336f 100644 --- a/src/vs/editor/contrib/gotoSymbol/peek/referencesController.ts +++ b/src/vs/editor/contrib/gotoSymbol/peek/referencesController.ts @@ -11,7 +11,7 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati import { IContextKey, IContextKeyService, RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import * as editorCommon from 'vs/editor/common/editorCommon'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ReferencesModel, OneReference } from '../referencesModel'; import { ReferenceWidget, LayoutData } from './referencesWidget'; @@ -24,10 +24,12 @@ import { getOuterEditor, PeekContext } from 'vs/editor/contrib/peekView/peekView import { IListService, WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KeyCode, KeyMod, KeyChord } from 'vs/base/common/keyCodes'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; export const ctxReferenceSearchVisible = new RawContextKey('referenceSearchVisible', false); -export abstract class ReferencesController implements editorCommon.IEditorContribution { +export abstract class ReferencesController implements IEditorContribution { static readonly ID = 'editor.contrib.referencesController'; @@ -162,7 +164,11 @@ export abstract class ReferencesController implements editorCommon.IEditorContri let pos = new Position(range.startLineNumber, range.startColumn); let selection = this._model.nearestReference(uri, pos); if (selection) { - return this._widget.setSelection(selection); + return this._widget.setSelection(selection).then(() => { + if (this._widget && this._editor.getOption(EditorOption.peekWidgetFocusInlineEditor)) { + this._widget.focusOnPreviewEditor(); + } + }); } } return undefined; @@ -200,10 +206,13 @@ export abstract class ReferencesController implements editorCommon.IEditorContri } const target = this._model.nextOrPreviousReference(source, fwd); const editorFocus = this._editor.hasTextFocus(); + const previewEditorFocus = this._widget.isPreviewEditorFocused(); await this._widget.setSelection(target); await this._gotoReference(target); if (editorFocus) { this._editor.focus(); + } else if (this._widget && previewEditorFocus) { + this._widget.focusOnPreviewEditor(); } } @@ -292,7 +301,7 @@ function withController(accessor: ServicesAccessor, fn: (controller: ReferencesC KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'changePeekFocus', - weight: KeybindingWeight.WorkbenchContrib + 50, + weight: KeybindingWeight.EditorContrib, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.F2), when: ContextKeyExpr.or(ctxReferenceSearchVisible, PeekContext.inPeekEditor), handler(accessor) { @@ -304,23 +313,10 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'goToNextReference', - weight: KeybindingWeight.WorkbenchContrib + 50, + weight: KeybindingWeight.EditorContrib - 10, primary: KeyCode.F4, secondary: [KeyCode.F12], - when: ctxReferenceSearchVisible, - handler(accessor) { - withController(accessor, controller => { - controller.goToNextOrPreviousReference(true); - }); - } -}); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'goToNextReferenceFromEmbeddedEditor', - weight: KeybindingWeight.EditorContrib + 50, - primary: KeyCode.F4, - secondary: [KeyCode.F12], - when: PeekContext.inPeekEditor, + when: ContextKeyExpr.or(ctxReferenceSearchVisible, PeekContext.inPeekEditor), handler(accessor) { withController(accessor, controller => { controller.goToNextOrPreviousReference(true); @@ -330,10 +326,10 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'goToPreviousReference', - weight: KeybindingWeight.WorkbenchContrib + 50, + weight: KeybindingWeight.EditorContrib - 10, primary: KeyMod.Shift | KeyCode.F4, secondary: [KeyMod.Shift | KeyCode.F12], - when: ctxReferenceSearchVisible, + when: ContextKeyExpr.or(ctxReferenceSearchVisible, PeekContext.inPeekEditor), handler(accessor) { withController(accessor, controller => { controller.goToNextOrPreviousReference(false); @@ -341,40 +337,31 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'goToPreviousReferenceFromEmbeddedEditor', - weight: KeybindingWeight.EditorContrib + 50, - primary: KeyMod.Shift | KeyCode.F4, - secondary: [KeyMod.Shift | KeyCode.F12], - when: PeekContext.inPeekEditor, - handler(accessor) { - withController(accessor, controller => { - controller.goToNextOrPreviousReference(false); - }); - } -}); +// commands that aren't needed anymore because there is now ContextKeyExpr.OR +CommandsRegistry.registerCommandAlias('goToNextReferenceFromEmbeddedEditor', 'goToNextReference'); +CommandsRegistry.registerCommandAlias('goToPreviousReferenceFromEmbeddedEditor', 'goToPreviousReference'); -KeybindingsRegistry.registerCommandAndKeybindingRule({ +// close +CommandsRegistry.registerCommandAlias('closeReferenceSearchEditor', 'closeReferenceSearch'); +CommandsRegistry.registerCommand( + 'closeReferenceSearch', + accessor => withController(accessor, controller => controller.closeWidget()) +); +KeybindingsRegistry.registerKeybindingRule({ + id: 'closeReferenceSearch', + weight: KeybindingWeight.EditorContrib - 101, + primary: KeyCode.Escape, + secondary: [KeyMod.Shift | KeyCode.Escape], + when: ContextKeyExpr.and(PeekContext.inPeekEditor, ContextKeyExpr.not('config.editor.stablePeek')) +}); +KeybindingsRegistry.registerKeybindingRule({ id: 'closeReferenceSearch', weight: KeybindingWeight.WorkbenchContrib + 50, primary: KeyCode.Escape, secondary: [KeyMod.Shift | KeyCode.Escape], - when: ContextKeyExpr.and(ctxReferenceSearchVisible, ContextKeyExpr.not('config.editor.stablePeek')), - handler(accessor: ServicesAccessor) { - withController(accessor, controller => controller.closeWidget()); - } + when: ContextKeyExpr.and(ctxReferenceSearchVisible, ContextKeyExpr.not('config.editor.stablePeek')) }); -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'closeReferenceSearchEditor', - weight: KeybindingWeight.EditorContrib - 101, - primary: KeyCode.Escape, - secondary: [KeyMod.Shift | KeyCode.Escape], - when: ContextKeyExpr.and(PeekContext.inPeekEditor, ContextKeyExpr.not('config.editor.stablePeek')), - handler(accessor: ServicesAccessor) { - withController(accessor, controller => controller.closeWidget()); - } -}); KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'openReferenceToSide', @@ -392,3 +379,11 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } } }); + +CommandsRegistry.registerCommand('openReference', (accessor) => { + const listService = accessor.get(IListService); + const focus = listService.lastFocusedList?.getFocus(); + if (Array.isArray(focus) && focus[0] instanceof OneReference) { + withController(accessor, controller => controller.openReference(focus[0], false)); + } +}); diff --git a/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts b/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts index 5fd2197761..318f804d27 100644 --- a/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts +++ b/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts @@ -17,7 +17,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IRange, Range } from 'vs/editor/common/core/range'; -import * as editorCommon from 'vs/editor/common/editorCommon'; +import { ScrollType } from 'vs/editor/common/editorCommon'; import { IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/model'; import { ModelDecorationOptions, TextModel } from 'vs/editor/common/model/textModel'; import { Location } from 'vs/editor/common/modes'; @@ -247,7 +247,7 @@ export class ReferenceWidget extends peekView.PeekViewWidget { } show(where: IRange) { - this.editor.revealRangeInCenterIfOutsideViewport(where, editorCommon.ScrollType.Smooth); + this.editor.revealRangeInCenterIfOutsideViewport(where, ScrollType.Smooth); super.show(where, this.layoutData.heightInLines || 18); } @@ -526,7 +526,7 @@ export class ReferenceWidget extends peekView.PeekViewWidget { // show in editor const model = ref.object; if (model) { - const scrollType = this._preview.getModel() === model.textEditorModel ? editorCommon.ScrollType.Smooth : editorCommon.ScrollType.Immediate; + const scrollType = this._preview.getModel() === model.textEditorModel ? ScrollType.Smooth : ScrollType.Immediate; const sel = Range.lift(reference.range).collapseToStart(); this._previewModelReference = ref; this._preview.setModel(model.textEditorModel); diff --git a/src/vs/editor/contrib/hover/modesContentHover.ts b/src/vs/editor/contrib/hover/modesContentHover.ts index 79c034408a..0c4fda743e 100644 --- a/src/vs/editor/contrib/hover/modesContentHover.ts +++ b/src/vs/editor/contrib/hover/modesContentHover.ts @@ -13,7 +13,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; -import { DocumentColorProvider, Hover as MarkdownHover, HoverProviderRegistry, IColor, TokenizationRegistry } from 'vs/editor/common/modes'; +import { DocumentColorProvider, Hover as MarkdownHover, HoverProviderRegistry, IColor, TokenizationRegistry, CodeActionTriggerType } from 'vs/editor/common/modes'; import { getColorPresentations } from 'vs/editor/contrib/colorPicker/color'; import { ColorDetector } from 'vs/editor/contrib/colorPicker/colorDetector'; import { ColorPickerModel } from 'vs/editor/contrib/colorPicker/colorPickerModel'; @@ -34,7 +34,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { getCodeActions, CodeActionSet } from 'vs/editor/contrib/codeAction/codeAction'; import { QuickFixAction, QuickFixController } from 'vs/editor/contrib/codeAction/codeActionCommands'; -import { CodeActionKind } from 'vs/editor/contrib/codeAction/types'; +import { CodeActionKind, CodeActionTrigger } from 'vs/editor/contrib/codeAction/types'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; @@ -186,6 +186,11 @@ class ModesContentComputer implements IHoverComputer { } } +const markerCodeActionTrigger: CodeActionTrigger = { + type: CodeActionTriggerType.Manual, + filter: { include: CodeActionKind.QuickFix } +}; + export class ModesContentHoverWidget extends ContentHoverWidget { static readonly ID = 'editor.contrib.modesContentHoverWidget'; @@ -575,7 +580,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { showing = true; const controller = QuickFixController.get(this._editor); const elementPosition = dom.getDomNodePagePosition(target); - controller.showCodeActions(actions, { + controller.showCodeActions(markerCodeActionTrigger, actions, { x: elementPosition.left + 6, y: elementPosition.top + elementPosition.height + 6 }); @@ -592,7 +597,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { return getCodeActions( this._editor.getModel()!, new Range(marker.startLineNumber, marker.startColumn, marker.endLineNumber, marker.endColumn), - { type: 'manual', filter: { include: CodeActionKind.QuickFix } }, + markerCodeActionTrigger, cancellationToken); }); } diff --git a/src/vs/editor/contrib/inPlaceReplace/inPlaceReplaceCommand.ts b/src/vs/editor/contrib/inPlaceReplace/inPlaceReplaceCommand.ts index ab43d59d20..6a46686da6 100644 --- a/src/vs/editor/contrib/inPlaceReplace/inPlaceReplaceCommand.ts +++ b/src/vs/editor/contrib/inPlaceReplace/inPlaceReplaceCommand.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { Selection } from 'vs/editor/common/core/selection'; -import * as editorCommon from 'vs/editor/common/editorCommon'; +import { ICommand, IEditOperationBuilder, ICursorStateComputerData } from 'vs/editor/common/editorCommon'; import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; -export class InPlaceReplaceCommand implements editorCommon.ICommand { +export class InPlaceReplaceCommand implements ICommand { private readonly _editRange: Range; private readonly _originalSelection: Selection; @@ -20,11 +20,11 @@ export class InPlaceReplaceCommand implements editorCommon.ICommand { this._text = text; } - public getEditOperations(model: ITextModel, builder: editorCommon.IEditOperationBuilder): void { + public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void { builder.addTrackedEditOperation(this._editRange, this._text); } - public computeCursorState(model: ITextModel, helper: editorCommon.ICursorStateComputerData): Selection { + public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection { const inverseEditOperations = helper.getInverseEditOperations(); const srcRange = inverseEditOperations[0].range; diff --git a/src/vs/editor/contrib/linesOperations/copyLinesCommand.ts b/src/vs/editor/contrib/linesOperations/copyLinesCommand.ts index 99aba47777..8d6131e0f1 100644 --- a/src/vs/editor/contrib/linesOperations/copyLinesCommand.ts +++ b/src/vs/editor/contrib/linesOperations/copyLinesCommand.ts @@ -5,10 +5,10 @@ import { Range } from 'vs/editor/common/core/range'; import { Selection, SelectionDirection } from 'vs/editor/common/core/selection'; -import * as editorCommon from 'vs/editor/common/editorCommon'; +import { ICommand, IEditOperationBuilder, ICursorStateComputerData } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; -export class CopyLinesCommand implements editorCommon.ICommand { +export class CopyLinesCommand implements ICommand { private readonly _selection: Selection; private readonly _isCopyingDown: boolean; @@ -27,7 +27,7 @@ export class CopyLinesCommand implements editorCommon.ICommand { this._endLineNumberDelta = 0; } - public getEditOperations(model: ITextModel, builder: editorCommon.IEditOperationBuilder): void { + public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void { let s = this._selection; this._startLineNumberDelta = 0; @@ -61,7 +61,7 @@ export class CopyLinesCommand implements editorCommon.ICommand { this._selectionDirection = this._selection.getDirection(); } - public computeCursorState(model: ITextModel, helper: editorCommon.ICursorStateComputerData): Selection { + public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection { let result = helper.getTrackedSelection(this._selectionId!); if (this._startLineNumberDelta !== 0 || this._endLineNumberDelta !== 0) { diff --git a/src/vs/editor/contrib/linesOperations/sortLinesCommand.ts b/src/vs/editor/contrib/linesOperations/sortLinesCommand.ts index 48b63cfc05..0f80cf7aec 100644 --- a/src/vs/editor/contrib/linesOperations/sortLinesCommand.ts +++ b/src/vs/editor/contrib/linesOperations/sortLinesCommand.ts @@ -6,10 +6,10 @@ 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 * as editorCommon from 'vs/editor/common/editorCommon'; +import { ICommand, IEditOperationBuilder, ICursorStateComputerData } from 'vs/editor/common/editorCommon'; import { IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model'; -export class SortLinesCommand implements editorCommon.ICommand { +export class SortLinesCommand implements ICommand { private static _COLLATOR: Intl.Collator | null = null; public static getCollator(): Intl.Collator { @@ -29,7 +29,7 @@ export class SortLinesCommand implements editorCommon.ICommand { this.selectionId = null; } - public getEditOperations(model: ITextModel, builder: editorCommon.IEditOperationBuilder): void { + public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void { let op = sortLines(model, this.selection, this.descending); if (op) { builder.addEditOperation(op.range, op.text); @@ -38,7 +38,7 @@ export class SortLinesCommand implements editorCommon.ICommand { this.selectionId = builder.trackSelection(this.selection); } - public computeCursorState(model: ITextModel, helper: editorCommon.ICursorStateComputerData): Selection { + public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection { return helper.getTrackedSelection(this.selectionId!); } diff --git a/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts b/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts index 30904395a7..ae175c2504 100644 --- a/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts @@ -12,6 +12,19 @@ import { ITextModel } from 'vs/editor/common/model'; import { TitleCaseAction, DeleteAllLeftAction, DeleteAllRightAction, IndentLinesAction, InsertLineAfterAction, InsertLineBeforeAction, JoinLinesAction, LowerCaseAction, SortLinesAscendingAction, SortLinesDescendingAction, TransposeAction, UpperCaseAction, DeleteLinesAction } from 'vs/editor/contrib/linesOperations/linesOperations'; import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; +import type { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorAction } from 'vs/editor/browser/editorExtensions'; + +function assertSelection(editor: ICodeEditor, expected: Selection | Selection[]): void { + if (!Array.isArray(expected)) { + expected = [expected]; + } + assert.deepEqual(editor.getSelections(), expected); +} + +function executeAction(action: EditorAction, editor: ICodeEditor): void { + action.run(null!, editor, undefined); +} suite('Editor Contrib - Line Operations', () => { suite('SortLinesAscendingAction', () => { @@ -26,13 +39,13 @@ suite('Editor Contrib - Line Operations', () => { let sortLinesAscendingAction = new SortLinesAscendingAction(); editor.setSelection(new Selection(1, 1, 3, 5)); - sortLinesAscendingAction.run(null!, editor); + executeAction(sortLinesAscendingAction, editor); assert.deepEqual(model.getLinesContent(), [ 'alpha', 'beta', 'omicron' ]); - assert.deepEqual(editor.getSelection()!.toString(), new Selection(1, 1, 3, 7).toString()); + assertSelection(editor, new Selection(1, 1, 3, 7)); }); }); @@ -51,7 +64,7 @@ suite('Editor Contrib - Line Operations', () => { let sortLinesAscendingAction = new SortLinesAscendingAction(); editor.setSelections([new Selection(1, 1, 3, 5), new Selection(5, 1, 7, 5)]); - sortLinesAscendingAction.run(null!, editor); + executeAction(sortLinesAscendingAction, editor); assert.deepEqual(model.getLinesContent(), [ 'alpha', 'beta', @@ -84,13 +97,13 @@ suite('Editor Contrib - Line Operations', () => { let sortLinesDescendingAction = new SortLinesDescendingAction(); editor.setSelection(new Selection(1, 1, 3, 7)); - sortLinesDescendingAction.run(null!, editor); + executeAction(sortLinesDescendingAction, editor); assert.deepEqual(model.getLinesContent(), [ 'omicron', 'beta', 'alpha' ]); - assert.deepEqual(editor.getSelection()!.toString(), new Selection(1, 1, 3, 5).toString()); + assertSelection(editor, new Selection(1, 1, 3, 5)); }); }); @@ -109,7 +122,7 @@ suite('Editor Contrib - Line Operations', () => { let sortLinesDescendingAction = new SortLinesDescendingAction(); editor.setSelections([new Selection(1, 1, 3, 7), new Selection(5, 1, 7, 7)]); - sortLinesDescendingAction.run(null!, editor); + executeAction(sortLinesDescendingAction, editor); assert.deepEqual(model.getLinesContent(), [ 'omicron', 'beta', @@ -143,13 +156,13 @@ suite('Editor Contrib - Line Operations', () => { let deleteAllLeftAction = new DeleteAllLeftAction(); editor.setSelection(new Selection(1, 2, 1, 2)); - deleteAllLeftAction.run(null!, editor); - assert.equal(model.getLineContent(1), 'ne', '001'); + executeAction(deleteAllLeftAction, editor); + assert.equal(model.getLineContent(1), 'ne'); editor.setSelections([new Selection(2, 2, 2, 2), new Selection(3, 2, 3, 2)]); - deleteAllLeftAction.run(null!, editor); - assert.equal(model.getLineContent(2), 'wo', '002'); - assert.equal(model.getLineContent(3), 'hree', '003'); + executeAction(deleteAllLeftAction, editor); + assert.equal(model.getLineContent(2), 'wo'); + assert.equal(model.getLineContent(3), 'hree'); }); }); @@ -164,16 +177,16 @@ suite('Editor Contrib - Line Operations', () => { let deleteAllLeftAction = new DeleteAllLeftAction(); editor.setSelection(new Selection(2, 1, 2, 1)); - deleteAllLeftAction.run(null!, editor); - assert.equal(model.getLineContent(1), 'onetwo', '001'); + executeAction(deleteAllLeftAction, editor); + assert.equal(model.getLineContent(1), 'onetwo'); editor.setSelections([new Selection(1, 1, 1, 1), new Selection(2, 1, 2, 1)]); - deleteAllLeftAction.run(null!, editor); + executeAction(deleteAllLeftAction, editor); assert.equal(model.getLinesContent()[0], 'onetwothree'); assert.equal(model.getLinesContent().length, 1); editor.setSelection(new Selection(1, 1, 1, 1)); - deleteAllLeftAction.run(null!, editor); + executeAction(deleteAllLeftAction, editor); assert.equal(model.getLinesContent()[0], 'onetwothree'); }); }); @@ -197,7 +210,7 @@ suite('Editor Contrib - Line Operations', () => { editor.setSelections([beforeSecondWasoSelection, endOfBCCSelection, endOfNonono]); - deleteAllLeftAction.run(null!, editor); + executeAction(deleteAllLeftAction, editor); let selections = editor.getSelections()!; assert.equal(model.getLineContent(2), ''); @@ -225,7 +238,7 @@ suite('Editor Contrib - Line Operations', () => { selections[2].endColumn ], [5, 1, 5, 1]); - deleteAllLeftAction.run(null!, editor); + executeAction(deleteAllLeftAction, editor); selections = editor.getSelections()!; assert.equal(model.getLineContent(1), 'hi my name is Carlos Matos waso waso'); @@ -263,24 +276,24 @@ suite('Editor Contrib - Line Operations', () => { let deleteAllLeftAction = new DeleteAllLeftAction(); editor.setSelections([new Selection(1, 2, 1, 2), new Selection(1, 4, 1, 4)]); - deleteAllLeftAction.run(null!, editor); - assert.equal(model.getLineContent(1), 'lo', '001'); + executeAction(deleteAllLeftAction, editor); + assert.equal(model.getLineContent(1), 'lo'); editor.setSelections([new Selection(2, 2, 2, 2), new Selection(2, 4, 2, 5)]); - deleteAllLeftAction.run(null!, editor); - assert.equal(model.getLineContent(2), 'd', '002'); + executeAction(deleteAllLeftAction, editor); + assert.equal(model.getLineContent(2), 'd'); editor.setSelections([new Selection(3, 2, 3, 5), new Selection(3, 7, 3, 7)]); - deleteAllLeftAction.run(null!, editor); - assert.equal(model.getLineContent(3), 'world', '003'); + executeAction(deleteAllLeftAction, editor); + assert.equal(model.getLineContent(3), 'world'); editor.setSelections([new Selection(4, 3, 4, 3), new Selection(4, 5, 5, 4)]); - deleteAllLeftAction.run(null!, editor); - assert.equal(model.getLineContent(4), 'jour', '004'); + executeAction(deleteAllLeftAction, editor); + assert.equal(model.getLineContent(4), 'jour'); editor.setSelections([new Selection(5, 3, 6, 3), new Selection(6, 5, 7, 5), new Selection(7, 7, 7, 7)]); - deleteAllLeftAction.run(null!, editor); - assert.equal(model.getLineContent(5), 'world', '005'); + executeAction(deleteAllLeftAction, editor); + assert.equal(model.getLineContent(5), 'world'); }); }); @@ -300,7 +313,7 @@ suite('Editor Contrib - Line Operations', () => { assert.equal(model.getLineContent(1), 'Typing some text here on line one'); assert.deepEqual(editor.getSelection(), new Selection(1, 31, 1, 31)); - deleteAllLeftAction.run(null!, editor); + executeAction(deleteAllLeftAction, editor); assert.equal(model.getLineContent(1), 'one'); assert.deepEqual(editor.getSelection(), new Selection(1, 1, 1, 1)); @@ -331,29 +344,29 @@ suite('Editor Contrib - Line Operations', () => { let joinLinesAction = new JoinLinesAction(); editor.setSelection(new Selection(1, 2, 1, 2)); - joinLinesAction.run(null!, editor); - assert.equal(model.getLineContent(1), 'hello world', '001'); - assert.deepEqual(editor.getSelection()!.toString(), new Selection(1, 6, 1, 6).toString(), '002'); + executeAction(joinLinesAction, editor); + assert.equal(model.getLineContent(1), 'hello world'); + assertSelection(editor, new Selection(1, 6, 1, 6)); editor.setSelection(new Selection(2, 2, 2, 2)); - joinLinesAction.run(null!, editor); - assert.equal(model.getLineContent(2), 'hello world', '003'); - assert.deepEqual(editor.getSelection()!.toString(), new Selection(2, 7, 2, 7).toString(), '004'); + executeAction(joinLinesAction, editor); + assert.equal(model.getLineContent(2), 'hello world'); + assertSelection(editor, new Selection(2, 7, 2, 7)); editor.setSelection(new Selection(3, 2, 3, 2)); - joinLinesAction.run(null!, editor); - assert.equal(model.getLineContent(3), 'hello world', '005'); - assert.deepEqual(editor.getSelection()!.toString(), new Selection(3, 7, 3, 7).toString(), '006'); + executeAction(joinLinesAction, editor); + assert.equal(model.getLineContent(3), 'hello world'); + assertSelection(editor, new Selection(3, 7, 3, 7)); editor.setSelection(new Selection(4, 2, 5, 3)); - joinLinesAction.run(null!, editor); - assert.equal(model.getLineContent(4), 'hello world', '007'); - assert.deepEqual(editor.getSelection()!.toString(), new Selection(4, 2, 4, 8).toString(), '008'); + executeAction(joinLinesAction, editor); + assert.equal(model.getLineContent(4), 'hello world'); + assertSelection(editor, new Selection(4, 2, 4, 8)); editor.setSelection(new Selection(5, 1, 7, 3)); - joinLinesAction.run(null!, editor); - assert.equal(model.getLineContent(5), 'hello world', '009'); - assert.deepEqual(editor.getSelection()!.toString(), new Selection(5, 1, 5, 3).toString(), '010'); + executeAction(joinLinesAction, editor); + assert.equal(model.getLineContent(5), 'hello world'); + assertSelection(editor, new Selection(5, 1, 5, 3)); }); }); @@ -367,10 +380,10 @@ suite('Editor Contrib - Line Operations', () => { let joinLinesAction = new JoinLinesAction(); editor.setSelection(new Selection(2, 1, 2, 1)); - joinLinesAction.run(null!, editor); - assert.equal(model.getLineContent(1), 'hello', '001'); - assert.equal(model.getLineContent(2), 'world', '002'); - assert.deepEqual(editor.getSelection()!.toString(), new Selection(2, 6, 2, 6).toString(), '003'); + executeAction(joinLinesAction, editor); + assert.equal(model.getLineContent(1), 'hello'); + assert.equal(model.getLineContent(2), 'world'); + assertSelection(editor, new Selection(2, 6, 2, 6)); }); }); @@ -402,19 +415,16 @@ suite('Editor Contrib - Line Operations', () => { new Selection(10, 1, 10, 1) ]); - joinLinesAction.run(null!, editor); - assert.equal(model.getLinesContent().join('\n'), 'hello world\nhello world\nhello world\nhello world\n\nhello world', '001'); - assert.deepEqual(editor.getSelections()!.toString(), [ + executeAction(joinLinesAction, editor); + assert.equal(model.getLinesContent().join('\n'), 'hello world\nhello world\nhello world\nhello world\n\nhello world'); + assertSelection(editor, [ /** primary cursor */ new Selection(3, 4, 3, 8), new Selection(1, 6, 1, 6), new Selection(2, 2, 2, 8), new Selection(4, 5, 4, 9), new Selection(6, 1, 6, 1) - ].toString(), '002'); - - /** primary cursor */ - assert.deepEqual(editor.getSelection()!.toString(), new Selection(3, 4, 3, 8).toString(), '003'); + ]); }); }); @@ -433,7 +443,7 @@ suite('Editor Contrib - Line Operations', () => { assert.equal(model.getLineContent(1), 'hello my dear'); assert.deepEqual(editor.getSelection(), new Selection(1, 14, 1, 14)); - joinLinesAction.run(null!, editor); + executeAction(joinLinesAction, editor); assert.equal(model.getLineContent(1), 'hello my dear world'); assert.deepEqual(editor.getSelection(), new Selection(1, 14, 1, 14)); @@ -456,29 +466,29 @@ suite('Editor Contrib - Line Operations', () => { let transposeAction = new TransposeAction(); editor.setSelection(new Selection(1, 1, 1, 1)); - transposeAction.run(null!, editor); - assert.equal(model.getLineContent(1), 'hello world', '001'); - assert.deepEqual(editor.getSelection()!.toString(), new Selection(1, 2, 1, 2).toString(), '002'); + executeAction(transposeAction, editor); + assert.equal(model.getLineContent(1), 'hello world'); + assertSelection(editor, new Selection(1, 2, 1, 2)); editor.setSelection(new Selection(1, 6, 1, 6)); - transposeAction.run(null!, editor); - assert.equal(model.getLineContent(1), 'hell oworld', '003'); - assert.deepEqual(editor.getSelection()!.toString(), new Selection(1, 7, 1, 7).toString(), '004'); + executeAction(transposeAction, editor); + assert.equal(model.getLineContent(1), 'hell oworld'); + assertSelection(editor, new Selection(1, 7, 1, 7)); editor.setSelection(new Selection(1, 12, 1, 12)); - transposeAction.run(null!, editor); - assert.equal(model.getLineContent(1), 'hell oworl', '005'); - assert.deepEqual(editor.getSelection()!.toString(), new Selection(2, 2, 2, 2).toString(), '006'); + executeAction(transposeAction, editor); + assert.equal(model.getLineContent(1), 'hell oworl'); + assertSelection(editor, new Selection(2, 2, 2, 2)); editor.setSelection(new Selection(3, 1, 3, 1)); - transposeAction.run(null!, editor); - assert.equal(model.getLineContent(3), '', '007'); - assert.deepEqual(editor.getSelection()!.toString(), new Selection(4, 1, 4, 1).toString(), '008'); + executeAction(transposeAction, editor); + assert.equal(model.getLineContent(3), ''); + assertSelection(editor, new Selection(4, 1, 4, 1)); editor.setSelection(new Selection(4, 2, 4, 2)); - transposeAction.run(null!, editor); - assert.equal(model.getLineContent(4), ' ', '009'); - assert.deepEqual(editor.getSelection()!.toString(), new Selection(4, 3, 4, 3).toString(), '010'); + executeAction(transposeAction, editor); + assert.equal(model.getLineContent(4), ' '); + assertSelection(editor, new Selection(4, 3, 4, 3)); } ); @@ -498,24 +508,24 @@ suite('Editor Contrib - Line Operations', () => { let transposeAction = new TransposeAction(); editor.setSelection(new Selection(1, 1, 1, 1)); - transposeAction.run(null!, editor); - assert.equal(model.getLineContent(2), '', '011'); - assert.deepEqual(editor.getSelection()!.toString(), new Selection(2, 1, 2, 1).toString(), '012'); + executeAction(transposeAction, editor); + assert.equal(model.getLineContent(2), ''); + assertSelection(editor, new Selection(2, 1, 2, 1)); editor.setSelection(new Selection(3, 6, 3, 6)); - transposeAction.run(null!, editor); - assert.equal(model.getLineContent(4), 'oworld', '013'); - assert.deepEqual(editor.getSelection()!.toString(), new Selection(4, 2, 4, 2).toString(), '014'); + executeAction(transposeAction, editor); + assert.equal(model.getLineContent(4), 'oworld'); + assertSelection(editor, new Selection(4, 2, 4, 2)); editor.setSelection(new Selection(6, 12, 6, 12)); - transposeAction.run(null!, editor); - assert.equal(model.getLineContent(7), 'd', '015'); - assert.deepEqual(editor.getSelection()!.toString(), new Selection(7, 2, 7, 2).toString(), '016'); + executeAction(transposeAction, editor); + assert.equal(model.getLineContent(7), 'd'); + assertSelection(editor, new Selection(7, 2, 7, 2)); editor.setSelection(new Selection(8, 12, 8, 12)); - transposeAction.run(null!, editor); - assert.equal(model.getLineContent(8), 'hello world', '019'); - assert.deepEqual(editor.getSelection()!.toString(), new Selection(8, 12, 8, 12).toString(), '020'); + executeAction(transposeAction, editor); + assert.equal(model.getLineContent(8), 'hello world'); + assertSelection(editor, new Selection(8, 12, 8, 12)); } ); }); @@ -532,44 +542,44 @@ suite('Editor Contrib - Line Operations', () => { let titlecaseAction = new TitleCaseAction(); editor.setSelection(new Selection(1, 1, 1, 12)); - uppercaseAction.run(null!, editor); - assert.equal(model.getLineContent(1), 'HELLO WORLD', '001'); - assert.deepEqual(editor.getSelection()!.toString(), new Selection(1, 1, 1, 12).toString(), '002'); + executeAction(uppercaseAction, editor); + assert.equal(model.getLineContent(1), 'HELLO WORLD'); + assertSelection(editor, new Selection(1, 1, 1, 12)); editor.setSelection(new Selection(1, 1, 1, 12)); - lowercaseAction.run(null!, editor); - assert.equal(model.getLineContent(1), 'hello world', '003'); - assert.deepEqual(editor.getSelection()!.toString(), new Selection(1, 1, 1, 12).toString(), '004'); + executeAction(lowercaseAction, editor); + assert.equal(model.getLineContent(1), 'hello world'); + assertSelection(editor, new Selection(1, 1, 1, 12)); editor.setSelection(new Selection(1, 3, 1, 3)); - uppercaseAction.run(null!, editor); - assert.equal(model.getLineContent(1), 'HELLO world', '005'); - assert.deepEqual(editor.getSelection()!.toString(), new Selection(1, 3, 1, 3).toString(), '006'); + executeAction(uppercaseAction, editor); + assert.equal(model.getLineContent(1), 'HELLO world'); + assertSelection(editor, new Selection(1, 3, 1, 3)); editor.setSelection(new Selection(1, 4, 1, 4)); - lowercaseAction.run(null!, editor); - assert.equal(model.getLineContent(1), 'hello world', '007'); - assert.deepEqual(editor.getSelection()!.toString(), new Selection(1, 4, 1, 4).toString(), '008'); + executeAction(lowercaseAction, editor); + assert.equal(model.getLineContent(1), 'hello world'); + assertSelection(editor, new Selection(1, 4, 1, 4)); editor.setSelection(new Selection(1, 1, 1, 12)); - titlecaseAction.run(null!, editor); - assert.equal(model.getLineContent(1), 'Hello World', '009'); - assert.deepEqual(editor.getSelection()!.toString(), new Selection(1, 1, 1, 12).toString(), '010'); + executeAction(titlecaseAction, editor); + assert.equal(model.getLineContent(1), 'Hello World'); + assertSelection(editor, new Selection(1, 1, 1, 12)); editor.setSelection(new Selection(2, 1, 2, 6)); - uppercaseAction.run(null!, editor); - assert.equal(model.getLineContent(2), 'ÖÇŞĞÜ', '011'); - assert.deepEqual(editor.getSelection()!.toString(), new Selection(2, 1, 2, 6).toString(), '012'); + executeAction(uppercaseAction, editor); + assert.equal(model.getLineContent(2), 'ÖÇŞĞÜ'); + assertSelection(editor, new Selection(2, 1, 2, 6)); editor.setSelection(new Selection(2, 1, 2, 6)); - lowercaseAction.run(null!, editor); - assert.equal(model.getLineContent(2), 'öçşğü', '013'); - assert.deepEqual(editor.getSelection()!.toString(), new Selection(2, 1, 2, 6).toString(), '014'); + executeAction(lowercaseAction, editor); + assert.equal(model.getLineContent(2), 'öçşğü'); + assertSelection(editor, new Selection(2, 1, 2, 6)); editor.setSelection(new Selection(2, 1, 2, 6)); - titlecaseAction.run(null!, editor); - assert.equal(model.getLineContent(2), 'Öçşğü', '015'); - assert.deepEqual(editor.getSelection()!.toString(), new Selection(2, 1, 2, 6).toString(), '016'); + executeAction(titlecaseAction, editor); + assert.equal(model.getLineContent(2), 'Öçşğü'); + assertSelection(editor, new Selection(2, 1, 2, 6)); } ); @@ -586,27 +596,27 @@ suite('Editor Contrib - Line Operations', () => { let titlecaseAction = new TitleCaseAction(); editor.setSelection(new Selection(1, 1, 1, 12)); - titlecaseAction.run(null!, editor); + executeAction(titlecaseAction, editor); assert.equal(model.getLineContent(1), 'Foo Bar Baz'); editor.setSelection(new Selection(2, 1, 2, 12)); - titlecaseAction.run(null!, editor); + executeAction(titlecaseAction, editor); assert.equal(model.getLineContent(2), 'Foo\'Bar\'Baz'); editor.setSelection(new Selection(3, 1, 3, 12)); - titlecaseAction.run(null!, editor); + executeAction(titlecaseAction, editor); assert.equal(model.getLineContent(3), 'Foo[Bar]Baz'); editor.setSelection(new Selection(4, 1, 4, 12)); - titlecaseAction.run(null!, editor); + executeAction(titlecaseAction, editor); assert.equal(model.getLineContent(4), 'Foo`Bar~Baz'); editor.setSelection(new Selection(5, 1, 5, 12)); - titlecaseAction.run(null!, editor); + executeAction(titlecaseAction, editor); assert.equal(model.getLineContent(5), 'Foo^Bar%Baz'); editor.setSelection(new Selection(6, 1, 6, 12)); - titlecaseAction.run(null!, editor); + executeAction(titlecaseAction, editor); assert.equal(model.getLineContent(6), 'Foo$Bar!Baz'); } ); @@ -621,24 +631,24 @@ suite('Editor Contrib - Line Operations', () => { let lowercaseAction = new LowerCaseAction(); editor.setSelection(new Selection(1, 1, 1, 1)); - uppercaseAction.run(null!, editor); - assert.equal(model.getLineContent(1), '', '013'); - assert.deepEqual(editor.getSelection()!.toString(), new Selection(1, 1, 1, 1).toString(), '014'); + executeAction(uppercaseAction, editor); + assert.equal(model.getLineContent(1), ''); + assertSelection(editor, new Selection(1, 1, 1, 1)); editor.setSelection(new Selection(1, 1, 1, 1)); - lowercaseAction.run(null!, editor); - assert.equal(model.getLineContent(1), '', '015'); - assert.deepEqual(editor.getSelection()!.toString(), new Selection(1, 1, 1, 1).toString(), '016'); + executeAction(lowercaseAction, editor); + assert.equal(model.getLineContent(1), ''); + assertSelection(editor, new Selection(1, 1, 1, 1)); editor.setSelection(new Selection(2, 2, 2, 2)); - uppercaseAction.run(null!, editor); - assert.equal(model.getLineContent(2), ' ', '017'); - assert.deepEqual(editor.getSelection()!.toString(), new Selection(2, 2, 2, 2).toString(), '018'); + executeAction(uppercaseAction, editor); + assert.equal(model.getLineContent(2), ' '); + assertSelection(editor, new Selection(2, 2, 2, 2)); editor.setSelection(new Selection(2, 2, 2, 2)); - lowercaseAction.run(null!, editor); - assert.equal(model.getLineContent(2), ' ', '019'); - assert.deepEqual(editor.getSelection()!.toString(), new Selection(2, 2, 2, 2).toString(), '020'); + executeAction(lowercaseAction, editor); + assert.equal(model.getLineContent(2), ' '); + assertSelection(editor, new Selection(2, 2, 2, 2)); } ); }); @@ -649,17 +659,17 @@ suite('Editor Contrib - Line Operations', () => { const model = editor.getModel()!; const action = new DeleteAllRightAction(); - action.run(null!, editor); + executeAction(action, editor); assert.deepEqual(model.getLinesContent(), ['']); assert.deepEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); editor.setSelection(new Selection(1, 1, 1, 1)); - action.run(null!, editor); + executeAction(action, editor); assert.deepEqual(model.getLinesContent(), ['']); assert.deepEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); editor.setSelections([new Selection(1, 1, 1, 1), new Selection(1, 1, 1, 1), new Selection(1, 1, 1, 1)]); - action.run(null!, editor); + executeAction(action, editor); assert.deepEqual(model.getLinesContent(), ['']); assert.deepEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); }); @@ -674,17 +684,17 @@ suite('Editor Contrib - Line Operations', () => { const action = new DeleteAllRightAction(); editor.setSelection(new Selection(1, 2, 1, 5)); - action.run(null!, editor); + executeAction(action, editor); assert.deepEqual(model.getLinesContent(), ['ho', 'world']); assert.deepEqual(editor.getSelections(), [new Selection(1, 2, 1, 2)]); editor.setSelection(new Selection(1, 1, 2, 4)); - action.run(null!, editor); + executeAction(action, editor); assert.deepEqual(model.getLinesContent(), ['ld']); assert.deepEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); editor.setSelection(new Selection(1, 1, 1, 3)); - action.run(null!, editor); + executeAction(action, editor); assert.deepEqual(model.getLinesContent(), ['']); assert.deepEqual(editor.getSelections(), [new Selection(1, 1, 1, 1)]); }); @@ -699,12 +709,12 @@ suite('Editor Contrib - Line Operations', () => { const action = new DeleteAllRightAction(); editor.setSelection(new Selection(1, 3, 1, 3)); - action.run(null!, editor); + executeAction(action, editor); assert.deepEqual(model.getLinesContent(), ['he', 'world']); assert.deepEqual(editor.getSelections(), [new Selection(1, 3, 1, 3)]); editor.setSelection(new Selection(2, 1, 2, 1)); - action.run(null!, editor); + executeAction(action, editor); assert.deepEqual(model.getLinesContent(), ['he', '']); assert.deepEqual(editor.getSelections(), [new Selection(2, 1, 2, 1)]); }); @@ -719,17 +729,17 @@ suite('Editor Contrib - Line Operations', () => { const action = new DeleteAllRightAction(); editor.setSelection(new Selection(1, 6, 1, 6)); - action.run(null!, editor); + executeAction(action, editor); assert.deepEqual(model.getLinesContent(), ['helloworld']); assert.deepEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); editor.setSelection(new Selection(1, 6, 1, 6)); - action.run(null!, editor); + executeAction(action, editor); assert.deepEqual(model.getLinesContent(), ['hello']); assert.deepEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); editor.setSelection(new Selection(1, 6, 1, 6)); - action.run(null!, editor); + executeAction(action, editor); assert.deepEqual(model.getLinesContent(), ['hello']); assert.deepEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]); }); @@ -749,34 +759,34 @@ suite('Editor Contrib - Line Operations', () => { new Selection(1, 6, 1, 6), new Selection(3, 4, 3, 4), ]); - action.run(null!, editor); + executeAction(action, editor); assert.deepEqual(model.getLinesContent(), ['hethere', 'wor']); assert.deepEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3), new Selection(2, 4, 2, 4) ]); - action.run(null!, editor); + executeAction(action, editor); assert.deepEqual(model.getLinesContent(), ['he', 'wor']); assert.deepEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3), new Selection(2, 4, 2, 4) ]); - action.run(null!, editor); + executeAction(action, editor); assert.deepEqual(model.getLinesContent(), ['hewor']); assert.deepEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3), new Selection(1, 6, 1, 6) ]); - action.run(null!, editor); + executeAction(action, editor); assert.deepEqual(model.getLinesContent(), ['he']); assert.deepEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3) ]); - action.run(null!, editor); + executeAction(action, editor); assert.deepEqual(model.getLinesContent(), ['he']); assert.deepEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3) @@ -798,7 +808,7 @@ suite('Editor Contrib - Line Operations', () => { new Selection(1, 6, 1, 6), new Selection(3, 4, 3, 4), ]); - action.run(null!, editor); + executeAction(action, editor); assert.deepEqual(model.getLinesContent(), ['hethere', 'wor']); assert.deepEqual(editor.getSelections(), [ new Selection(1, 3, 1, 3), @@ -831,7 +841,7 @@ suite('Editor Contrib - Line Operations', () => { editor.setPosition(new Position(lineNumber, column)); let insertLineBeforeAction = new InsertLineBeforeAction(); - insertLineBeforeAction.run(null!, editor); + executeAction(insertLineBeforeAction, editor); callback(editor.getModel()!, cursor); }); } @@ -872,7 +882,7 @@ suite('Editor Contrib - Line Operations', () => { editor.setPosition(new Position(lineNumber, column)); let insertLineAfterAction = new InsertLineAfterAction(); - insertLineAfterAction.run(null!, editor); + executeAction(insertLineAfterAction, editor); callback(editor.getModel()!, cursor); }); } @@ -917,7 +927,7 @@ suite('Editor Contrib - Line Operations', () => { let indentLinesAction = new IndentLinesAction(); editor.setPosition(new Position(1, 2)); - indentLinesAction.run(null!, editor); + executeAction(indentLinesAction, editor); assert.equal(model.getLineContent(1), '\tfunction baz() {'); assert.deepEqual(editor.getSelection(), new Selection(1, 3, 1, 3)); @@ -942,7 +952,7 @@ suite('Editor Contrib - Line Operations', () => { const indentLinesAction = new IndentLinesAction(); editor.setPosition(new Position(1, 1)); - indentLinesAction.run(null!, editor); + executeAction(indentLinesAction, editor); assert.equal(model.getLineContent(1), '\tSome text'); assert.deepEqual(editor.getSelection(), new Selection(1, 2, 1, 2)); }); @@ -964,7 +974,7 @@ suite('Editor Contrib - Line Operations', () => { new Selection(3, 4, 3, 4), ]); const deleteLinesAction = new DeleteLinesAction(); - deleteLinesAction.run(null!, editor); + executeAction(deleteLinesAction, editor); assert.equal(editor.getValue(), 'a\nc'); }); @@ -976,7 +986,7 @@ suite('Editor Contrib - Line Operations', () => { withTestCodeEditor(initialText, {}, (editor) => { editor.setSelections(initialSelections); const deleteLinesAction = new DeleteLinesAction(); - deleteLinesAction.run(null!, editor); + executeAction(deleteLinesAction, editor); assert.equal(editor.getValue(), resultingText.join('\n')); assert.deepEqual(editor.getSelections(), resultingSelections); diff --git a/src/vs/editor/contrib/links/links.ts b/src/vs/editor/contrib/links/links.ts index 19b1df1e12..b1b4f8f6de 100644 --- a/src/vs/editor/contrib/links/links.ts +++ b/src/vs/editor/contrib/links/links.ts @@ -14,7 +14,7 @@ import * as platform from 'vs/base/common/platform'; import { ICodeEditor, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { Position } from 'vs/editor/common/core/position'; -import * as editorCommon from 'vs/editor/common/editorCommon'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { IModelDecorationsChangeAccessor, IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { LinkProviderRegistry } from 'vs/editor/common/modes'; @@ -97,7 +97,7 @@ class LinkOccurrence { } } -class LinkDetector implements editorCommon.IEditorContribution { +class LinkDetector implements IEditorContribution { public static readonly ID: string = 'editor.linkDetector'; diff --git a/src/vs/editor/contrib/message/messageController.ts b/src/vs/editor/contrib/message/messageController.ts index e5da4cc805..22de385f90 100644 --- a/src/vs/editor/contrib/message/messageController.ts +++ b/src/vs/editor/contrib/message/messageController.ts @@ -10,7 +10,7 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { IDisposable, Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { Range } from 'vs/editor/common/core/range'; -import * as editorCommon from 'vs/editor/common/editorCommon'; +import { IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon'; import { registerEditorContribution, EditorCommand, registerEditorCommand } from 'vs/editor/browser/editorExtensions'; import { ICodeEditor, IContentWidget, IContentWidgetPosition, ContentWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; @@ -19,7 +19,7 @@ import { registerThemingParticipant, HIGH_CONTRAST } from 'vs/platform/theme/com import { inputValidationInfoBorder, inputValidationInfoBackground, inputValidationInfoForeground } from 'vs/platform/theme/common/colorRegistry'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -export class MessageController extends Disposable implements editorCommon.IEditorContribution { +export class MessageController extends Disposable implements IEditorContribution { public static readonly ID = 'editor.contrib.messageController'; @@ -144,7 +144,7 @@ class MessageWidget implements IContentWidget { constructor(editor: ICodeEditor, { lineNumber, column }: IPosition, text: string) { this._editor = editor; - this._editor.revealLinesInCenterIfOutsideViewport(lineNumber, lineNumber, editorCommon.ScrollType.Smooth); + this._editor.revealLinesInCenterIfOutsideViewport(lineNumber, lineNumber, ScrollType.Smooth); this._position = { lineNumber, column: column - 1 }; this._domNode = document.createElement('div'); diff --git a/src/vs/editor/contrib/parameterHints/arrow-down-dark.svg b/src/vs/editor/contrib/parameterHints/arrow-down-dark.svg deleted file mode 100644 index b1a76d789e..0000000000 --- a/src/vs/editor/contrib/parameterHints/arrow-down-dark.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/editor/contrib/parameterHints/arrow-down.svg b/src/vs/editor/contrib/parameterHints/arrow-down.svg deleted file mode 100644 index d643403d75..0000000000 --- a/src/vs/editor/contrib/parameterHints/arrow-down.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/editor/contrib/parameterHints/arrow-up-dark.svg b/src/vs/editor/contrib/parameterHints/arrow-up-dark.svg deleted file mode 100644 index 43f31a0952..0000000000 --- a/src/vs/editor/contrib/parameterHints/arrow-up-dark.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/editor/contrib/parameterHints/arrow-up.svg b/src/vs/editor/contrib/parameterHints/arrow-up.svg deleted file mode 100644 index b8072bb31b..0000000000 --- a/src/vs/editor/contrib/parameterHints/arrow-up.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/editor/contrib/parameterHints/parameterHints.css b/src/vs/editor/contrib/parameterHints/parameterHints.css index 623baa27b5..9d3c535eaa 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHints.css +++ b/src/vs/editor/contrib/parameterHints/parameterHints.css @@ -54,7 +54,7 @@ white-space: initial; } -.monaco-editor .parameter-hints-widget .docs .markdown-docs p code { +.monaco-editor .parameter-hints-widget .docs .markdown-docs code { font-family: var(--monaco-monospace-font); } @@ -89,11 +89,6 @@ .monaco-editor .parameter-hints-widget .button.previous { bottom: 24px; - background-image: url('arrow-up.svg'); -} - -.monaco-editor .parameter-hints-widget .button.next { - background-image: url('arrow-down.svg'); } .monaco-editor .parameter-hints-widget .overloads { @@ -113,15 +108,3 @@ font-weight: bold; margin-right: 0.5em; } - -/*** VS Dark & High Contrast*/ - -.monaco-editor.hc-black .parameter-hints-widget .button.previous, -.monaco-editor.vs-dark .parameter-hints-widget .button.previous { - background-image: url('arrow-up-dark.svg'); -} - -.monaco-editor.hc-black .parameter-hints-widget .button.next, -.monaco-editor.vs-dark .parameter-hints-widget .button.next { - background-image: url('arrow-down-dark.svg'); -} diff --git a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts index 9765ac757b..b40d39f460 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts +++ b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts @@ -78,9 +78,9 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { wrapper.tabIndex = -1; const controls = dom.append(wrapper, $('.controls')); - const previous = dom.append(controls, $('.button.previous')); + const previous = dom.append(controls, $('.button.codicon.codicon-chevron-up')); const overloads = dom.append(controls, $('.overloads')); - const next = dom.append(controls, $('.button.next')); + const next = dom.append(controls, $('.button.codicon.codicon-chevron-down')); const onPreviousClick = stop(domEvent(previous, 'click')); this._register(onPreviousClick(this.previous, this)); diff --git a/src/vs/editor/contrib/rename/rename.ts b/src/vs/editor/contrib/rename/rename.ts index 56ae606fa0..8f47c0b36c 100644 --- a/src/vs/editor/contrib/rename/rename.ts +++ b/src/vs/editor/contrib/rename/rename.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { illegalArgument, onUnexpectedError } from 'vs/base/common/errors'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; -import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IEditorProgressService } from 'vs/platform/progress/common/progress'; import { registerEditorAction, registerEditorContribution, ServicesAccessor, EditorAction, EditorCommand, registerEditorCommand, registerDefaultLanguageCommand } from 'vs/editor/browser/editorExtensions'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; @@ -14,7 +14,6 @@ import { ITextModel } from 'vs/editor/common/model'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { RenameInputField, CONTEXT_RENAME_INPUT_VISIBLE } from './renameInputField'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; import { WorkspaceEdit, RenameProviderRegistry, RenameProvider, RenameLocation, Rejection } from 'vs/editor/common/modes'; import { Position, IPosition } from 'vs/editor/common/core/position'; import { alert } from 'vs/base/browser/ui/aria/aria'; @@ -30,6 +29,11 @@ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cance import { DisposableStore } from 'vs/base/common/lifecycle'; import { IdleValue, raceCancellation } from 'vs/base/common/async'; import { withNullAsUndefined } from 'vs/base/common/types'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IConfigurationRegistry, ConfigurationScope, Extensions } from 'vs/platform/configuration/common/configurationRegistry'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; class RenameSkeleton { @@ -109,13 +113,14 @@ class RenameController implements IEditorContribution { constructor( private readonly editor: ICodeEditor, + @IInstantiationService private readonly _instaService: IInstantiationService, @INotificationService private readonly _notificationService: INotificationService, @IBulkEditService private readonly _bulkEditService: IBulkEditService, @IEditorProgressService private readonly _progressService: IEditorProgressService, - @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @IThemeService private readonly _themeService: IThemeService, + @ILogService private readonly _logService: ILogService, + @ITextResourceConfigurationService private readonly _configService: ITextResourceConfigurationService, ) { - this._renameInputField = new IdleValue(() => this._dispoableStore.add(new RenameInputField(this.editor, this._themeService, this._contextKeyService))); + this._renameInputField = this._dispoableStore.add(new IdleValue(() => this._dispoableStore.add(this._instaService.createInstance(RenameInputField, this.editor, ['acceptRenameInput', 'acceptRenameInputWithPreview'])))); } dispose(): void { @@ -174,11 +179,12 @@ class RenameController implements IEditorContribution { selectionEnd = Math.min(loc.range.endColumn, selection.endColumn) - loc.range.startColumn; } - const newNameOrFocusFlag = await this._renameInputField.getValue().getInput(loc.range, loc.text, selectionStart, selectionEnd); + const supportPreview = this._configService.getValue(this.editor.getModel().uri, 'editor.rename.enablePreview'); + const inputFieldResult = await this._renameInputField.getValue().getInput(loc.range, loc.text, selectionStart, selectionEnd, supportPreview); - - if (typeof newNameOrFocusFlag === 'boolean') { - if (newNameOrFocusFlag) { + // no result, only hint to focus the editor or not + if (typeof inputFieldResult === 'boolean') { + if (inputFieldResult) { this.editor.focus(); } return undefined; @@ -186,7 +192,7 @@ class RenameController implements IEditorContribution { this.editor.focus(); - const renameOperation = raceCancellation(skeleton.provideRenameEdits(newNameOrFocusFlag, 0, [], this._cts.token), this._cts.token).then(async renameResult => { + const renameOperation = raceCancellation(skeleton.provideRenameEdits(inputFieldResult.newName, 0, [], this._cts.token), this._cts.token).then(async renameResult => { if (!renameResult || !this.editor.hasModel()) { return; @@ -197,16 +203,22 @@ class RenameController implements IEditorContribution { return; } - const editResult = await this._bulkEditService.apply(renameResult, { editor: this.editor }); - - // alert - if (editResult.ariaSummary) { - alert(nls.localize('aria', "Successfully renamed '{0}' to '{1}'. Summary: {2}", loc!.text, newNameOrFocusFlag, editResult.ariaSummary)); - } + this._bulkEditService.apply(renameResult, { + editor: this.editor, + showPreview: inputFieldResult.wantsPreview, + label: nls.localize('label', "Renaming '{0}'", loc?.text) + }).then(result => { + if (result.ariaSummary) { + alert(nls.localize('aria', "Successfully renamed '{0}' to '{1}'. Summary: {2}", loc!.text, inputFieldResult.newName, result.ariaSummary)); + } + }).catch(err => { + this._notificationService.error(nls.localize('rename.failedApply', "Rename failed to apply edits")); + this._logService.error(err); + }); }, err => { - this._notificationService.error(nls.localize('rename.failed', "Rename failed to execute.")); - return Promise.reject(err); + this._notificationService.error(nls.localize('rename.failed', "Rename failed to compute edits")); + this._logService.error(err); }); this._progressService.showWhile(renameOperation, 250); @@ -214,8 +226,8 @@ class RenameController implements IEditorContribution { } - acceptRenameInput(): void { - this._renameInputField.getValue().acceptInput(); + acceptRenameInput(wantsPreview: boolean): void { + this._renameInputField.getValue().acceptInput(wantsPreview); } cancelRenameInput(): void { @@ -282,7 +294,7 @@ const RenameCommand = EditorCommand.bindToContribution(RenameC registerEditorCommand(new RenameCommand({ id: 'acceptRenameInput', precondition: CONTEXT_RENAME_INPUT_VISIBLE, - handler: x => x.acceptRenameInput(), + handler: x => x.acceptRenameInput(false), kbOpts: { weight: KeybindingWeight.EditorContrib + 99, kbExpr: EditorContextKeys.focus, @@ -290,6 +302,17 @@ registerEditorCommand(new RenameCommand({ } })); +registerEditorCommand(new RenameCommand({ + id: 'acceptRenameInputWithPreview', + precondition: ContextKeyExpr.and(CONTEXT_RENAME_INPUT_VISIBLE, ContextKeyExpr.has('config.editor.rename.enablePreview')), + handler: x => x.acceptRenameInput(true), + kbOpts: { + weight: KeybindingWeight.EditorContrib + 99, + kbExpr: EditorContextKeys.focus, + primary: KeyMod.Shift + KeyCode.Enter + } +})); + registerEditorCommand(new RenameCommand({ id: 'cancelRenameInput', precondition: CONTEXT_RENAME_INPUT_VISIBLE, @@ -311,3 +334,17 @@ registerDefaultLanguageCommand('_executeDocumentRenameProvider', function (model } return rename(model, position, newName); }); + + +//todo@joh use editor options world +Registry.as(Extensions.Configuration).registerConfiguration({ + id: 'editor', + properties: { + 'editor.rename.enablePreview': { + scope: ConfigurationScope.RESOURCE_LANGUAGE, + description: nls.localize('enablePreview', "Enabe/disable the ability to preview changes before renaming"), + default: true, + type: 'boolean' + } + } +}); diff --git a/src/vs/editor/contrib/rename/renameInputField.css b/src/vs/editor/contrib/rename/renameInputField.css index 49959fc391..b4dac18ae4 100644 --- a/src/vs/editor/contrib/rename/renameInputField.css +++ b/src/vs/editor/contrib/rename/renameInputField.css @@ -8,6 +8,20 @@ color: inherit; } -.monaco-editor .rename-box .rename-input { +.monaco-editor .rename-box.preview { padding: 4px; } + +.monaco-editor .rename-box .rename-input { + padding: 4px; + width: calc(100% - 8px); +} + +.monaco-editor .rename-box .rename-label { + display: none; + opacity: .8; +} + +.monaco-editor .rename-box.preview .rename-label { + display: inherit; +} diff --git a/src/vs/editor/contrib/rename/renameInputField.ts b/src/vs/editor/contrib/rename/renameInputField.ts index cd480295e5..8c944f339d 100644 --- a/src/vs/editor/contrib/rename/renameInputField.ts +++ b/src/vs/editor/contrib/rename/renameInputField.ts @@ -4,166 +4,185 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./renameInputField'; -import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { ScrollType } from 'vs/editor/common/editorCommon'; import { localize } from 'vs/nls'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { inputBackground, inputBorder, inputForeground, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; +import { inputBackground, inputBorder, inputForeground, widgetShadow, editorWidgetBackground } from 'vs/platform/theme/common/colorRegistry'; import { ITheme, IThemeService } from 'vs/platform/theme/common/themeService'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { toggleClass } from 'vs/base/browser/dom'; export const CONTEXT_RENAME_INPUT_VISIBLE = new RawContextKey('renameInputVisible', false); -export class RenameInputField implements IContentWidget, IDisposable { +export interface RenameInputFieldResult { + newName: string; + wantsPreview?: boolean; +} + +export class RenameInputField implements IContentWidget { - private _editor: ICodeEditor; private _position?: Position; private _domNode?: HTMLElement; - private _inputField?: HTMLInputElement; + private _input?: HTMLInputElement; + private _label?: HTMLDivElement; private _visible?: boolean; private readonly _visibleContextKey: IContextKey; private readonly _disposables = new DisposableStore(); - // Editor.IContentWidget.allowEditorOverflow - allowEditorOverflow: boolean = true; + readonly allowEditorOverflow: boolean = true; constructor( - editor: ICodeEditor, - private readonly themeService: IThemeService, - contextKeyService: IContextKeyService, + private readonly _editor: ICodeEditor, + private readonly _acceptKeybindings: [string, string], + @IThemeService private readonly _themeService: IThemeService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IContextKeyService contextKeyService: IContextKeyService, ) { this._visibleContextKey = CONTEXT_RENAME_INPUT_VISIBLE.bindTo(contextKeyService); - this._editor = editor; this._editor.addContentWidget(this); - this._disposables.add(editor.onDidChangeConfiguration(e => { + this._disposables.add(this._editor.onDidChangeConfiguration(e => { if (e.hasChanged(EditorOption.fontInfo)) { - this.updateFont(); + this._updateFont(); } })); - this._disposables.add(themeService.onThemeChange(theme => this.onThemeChange(theme))); + this._disposables.add(_themeService.onThemeChange(this._updateStyles, this)); } - private onThemeChange(theme: ITheme): void { - this.updateStyles(theme); - } - - public dispose(): void { + dispose(): void { this._disposables.dispose(); this._editor.removeContentWidget(this); } - public getId(): string { + getId(): string { return '__renameInputWidget'; } - public getDomNode(): HTMLElement { + getDomNode(): HTMLElement { if (!this._domNode) { - this._inputField = document.createElement('input'); - this._inputField.className = 'rename-input'; - this._inputField.type = 'text'; - this._inputField.setAttribute('aria-label', localize('renameAriaLabel', "Rename input. Type new name and press Enter to commit.")); this._domNode = document.createElement('div'); - this._domNode.style.height = `${this._editor.getOption(EditorOption.lineHeight)}px`; this._domNode.className = 'monaco-editor rename-box'; - this._domNode.appendChild(this._inputField); - this.updateFont(); - this.updateStyles(this.themeService.getTheme()); + this._input = document.createElement('input'); + this._input.className = 'rename-input'; + this._input.type = 'text'; + this._input.setAttribute('aria-label', localize('renameAriaLabel', "Rename input. Type new name and press Enter to commit.")); + this._domNode.appendChild(this._input); + + this._label = document.createElement('div'); + this._label.className = 'rename-label'; + this._domNode.appendChild(this._label); + const updateLabel = () => { + const [accept, preview] = this._acceptKeybindings; + this._keybindingService.lookupKeybinding(accept); + this._label!.innerText = localize('label', "{0} to Rename, {1} to Preview", this._keybindingService.lookupKeybinding(accept)?.getLabel(), this._keybindingService.lookupKeybinding(preview)?.getLabel()); + }; + updateLabel(); + this._disposables.add(this._keybindingService.onDidUpdateKeybindings(updateLabel)); + + this._updateFont(); + this._updateStyles(this._themeService.getTheme()); } return this._domNode; } - private updateStyles(theme: ITheme): void { - if (!this._inputField) { + private _updateStyles(theme: ITheme): void { + if (!this._input || !this._domNode) { return; } - const background = theme.getColor(inputBackground); - const foreground = theme.getColor(inputForeground); const widgetShadowColor = theme.getColor(widgetShadow); + this._domNode.style.backgroundColor = String(theme.getColor(editorWidgetBackground) ?? ''); + this._domNode.style.boxShadow = widgetShadowColor ? ` 0 2px 8px ${widgetShadowColor}` : ''; + this._domNode.style.color = String(theme.getColor(inputForeground) ?? ''); + + this._input.style.backgroundColor = String(theme.getColor(inputBackground) ?? ''); + // this._input.style.color = String(theme.getColor(inputForeground) ?? ''); const border = theme.getColor(inputBorder); - - this._inputField.style.backgroundColor = background ? background.toString() : ''; - this._inputField.style.color = foreground ? foreground.toString() : null; - - this._inputField.style.borderWidth = border ? '1px' : '0px'; - this._inputField.style.borderStyle = border ? 'solid' : 'none'; - this._inputField.style.borderColor = border ? border.toString() : 'none'; - - this._domNode!.style.boxShadow = widgetShadowColor ? ` 0 2px 8px ${widgetShadowColor}` : ''; + this._input.style.borderWidth = border ? '1px' : '0px'; + this._input.style.borderStyle = border ? 'solid' : 'none'; + this._input.style.borderColor = border?.toString() ?? 'none'; } - private updateFont(): void { - if (!this._inputField) { + private _updateFont(): void { + if (!this._input || !this._label) { return; } const fontInfo = this._editor.getOption(EditorOption.fontInfo); - this._inputField.style.fontFamily = fontInfo.fontFamily; - this._inputField.style.fontWeight = fontInfo.fontWeight; - this._inputField.style.fontSize = `${fontInfo.fontSize}px`; + this._input.style.fontFamily = fontInfo.fontFamily; + this._input.style.fontWeight = fontInfo.fontWeight; + this._input.style.fontSize = `${fontInfo.fontSize}px`; + + this._label.style.fontSize = `${fontInfo.fontSize * 0.8}px`; } - public getPosition(): IContentWidgetPosition | null { - return this._visible - ? { position: this._position!, preference: [ContentWidgetPositionPreference.BELOW, ContentWidgetPositionPreference.ABOVE] } - : null; + getPosition(): IContentWidgetPosition | null { + if (!this._visible) { + return null; + } + return { + position: this._position!, + preference: [ContentWidgetPositionPreference.BELOW, ContentWidgetPositionPreference.ABOVE] + }; } - private _currentAcceptInput: (() => void) | null = null; - private _currentCancelInput: ((focusEditor: boolean) => void) | null = null; + private _currentAcceptInput?: (wantsPreview: boolean) => void; + private _currentCancelInput?: (focusEditor: boolean) => void; - public acceptInput(): void { + acceptInput(wantsPreview: boolean): void { if (this._currentAcceptInput) { - this._currentAcceptInput(); + this._currentAcceptInput(wantsPreview); } } - public cancelInput(focusEditor: boolean): void { + cancelInput(focusEditor: boolean): void { if (this._currentCancelInput) { this._currentCancelInput(focusEditor); } } - public getInput(where: IRange, value: string, selectionStart: number, selectionEnd: number): Promise { + getInput(where: IRange, value: string, selectionStart: number, selectionEnd: number, supportPreview: boolean): Promise { + + toggleClass(this._domNode!, 'preview', supportPreview); this._position = new Position(where.startLineNumber, where.startColumn); - this._inputField!.value = value; - this._inputField!.setAttribute('selectionStart', selectionStart.toString()); - this._inputField!.setAttribute('selectionEnd', selectionEnd.toString()); - this._inputField!.size = Math.max((where.endColumn - where.startColumn) * 1.1, 20); + this._input!.value = value; + this._input!.setAttribute('selectionStart', selectionStart.toString()); + this._input!.setAttribute('selectionEnd', selectionEnd.toString()); + this._input!.size = Math.max((where.endColumn - where.startColumn) * 1.1, 20); const disposeOnDone = new DisposableStore(); - const always = () => { - disposeOnDone.dispose(); - this._hide(); - }; - return new Promise(resolve => { + return new Promise(resolve => { this._currentCancelInput = (focusEditor) => { - this._currentAcceptInput = null; - this._currentCancelInput = null; + this._currentAcceptInput = undefined; + this._currentCancelInput = undefined; resolve(focusEditor); return true; }; - this._currentAcceptInput = () => { - if (this._inputField!.value.trim().length === 0 || this._inputField!.value === value) { + this._currentAcceptInput = (wantsPreview) => { + if (this._input!.value.trim().length === 0 || this._input!.value === value) { // empty or whitespace only or not changed this.cancelInput(true); return; } - this._currentAcceptInput = null; - this._currentCancelInput = null; - resolve(this._inputField!.value); + this._currentAcceptInput = undefined; + this._currentCancelInput = undefined; + resolve({ + newName: this._input!.value, + wantsPreview: supportPreview && wantsPreview + }); }; let onCursorChanged = () => { @@ -178,12 +197,9 @@ export class RenameInputField implements IContentWidget, IDisposable { this._show(); - }).then(newValue => { - always(); - return newValue; - }, err => { - always(); - return Promise.reject(err); + }).finally(() => { + disposeOnDone.dispose(); + this._hide(); }); } @@ -194,10 +210,10 @@ export class RenameInputField implements IContentWidget, IDisposable { this._editor.layoutContentWidget(this); setTimeout(() => { - this._inputField!.focus(); - this._inputField!.setSelectionRange( - parseInt(this._inputField!.getAttribute('selectionStart')!), - parseInt(this._inputField!.getAttribute('selectionEnd')!)); + this._input!.focus(); + this._input!.setSelectionRange( + parseInt(this._input!.getAttribute('selectionStart')!), + parseInt(this._input!.getAttribute('selectionEnd')!)); }, 100); } diff --git a/src/vs/editor/contrib/snippet/snippetParser.ts b/src/vs/editor/contrib/snippet/snippetParser.ts index 529860842a..f5dd81189d 100644 --- a/src/vs/editor/contrib/snippet/snippetParser.ts +++ b/src/vs/editor/contrib/snippet/snippetParser.ts @@ -605,7 +605,7 @@ export class SnippetParser { // fill in values for placeholders. the first placeholder of an index // that has a value defines the value for all placeholders with that index - const placeholderDefaultValues = new Map(); + const placeholderDefaultValues = new Map(); const incompletePlaceholders: Placeholder[] = []; let placeholderCount = 0; snippet.walk(marker => { diff --git a/src/vs/editor/contrib/suggest/media/suggest.css b/src/vs/editor/contrib/suggest/media/suggest.css index 0472b8bdc5..ca5abb77d6 100644 --- a/src/vs/editor/contrib/suggest/media/suggest.css +++ b/src/vs/editor/contrib/suggest/media/suggest.css @@ -162,7 +162,7 @@ opacity: 0.66; text-decoration: unset; } -.monaco-editor .suggest-widget .monaco-list .monaco-list-row .monaco-icon-label.deprecated > .monaco-icon-label-description-container { +.monaco-editor .suggest-widget .monaco-list .monaco-list-row .monaco-icon-label.deprecated > .monaco-icon-label-container > .monaco-icon-name-container { text-decoration: line-through; } diff --git a/src/vs/editor/contrib/suggest/suggest.ts b/src/vs/editor/contrib/suggest/suggest.ts index 3dd590a444..f89288f1c3 100644 --- a/src/vs/editor/contrib/suggest/suggest.ts +++ b/src/vs/editor/contrib/suggest/suggest.ts @@ -30,6 +30,7 @@ export class CompletionItem { _brand!: 'ISuggestionItem'; readonly resolve: (token: CancellationToken) => Promise; + isResolved: boolean = false; // readonly editStart: IPosition; @@ -47,6 +48,9 @@ export class CompletionItem { idx?: number; word?: string; + // + readonly isDetailsResolved: boolean; + constructor( readonly position: IPosition, readonly completion: modes.CompletionItem, @@ -70,18 +74,20 @@ export class CompletionItem { this.editReplaceEnd = new Position(completion.range.replace.endLineNumber, completion.range.replace.endColumn); } + this.isDetailsResolved = container.isDetailsResolved || typeof provider.resolveCompletionItem === 'undefined'; + // create the suggestion resolver const { resolveCompletionItem } = provider; if (typeof resolveCompletionItem !== 'function') { this.resolve = () => Promise.resolve(); + this.isResolved = true; } else { let cached: Promise | undefined; this.resolve = (token) => { if (!cached) { - let isDone = false; cached = Promise.resolve(resolveCompletionItem.call(provider, model, position, completion, token)).then(value => { assign(completion, value); - isDone = true; + this.isResolved = true; }, err => { if (isPromiseCanceledError(err)) { // the IPC queue will reject the request with the @@ -90,7 +96,7 @@ export class CompletionItem { } }); token.onCancellationRequested(() => { - if (!isDone) { + if (!this.isResolved) { // cancellation after the request has been // dispatched -> reset cache cached = undefined; diff --git a/src/vs/editor/contrib/suggest/suggestController.ts b/src/vs/editor/contrib/suggest/suggestController.ts index 4b6b58e531..82d0c851ac 100644 --- a/src/vs/editor/contrib/suggest/suggestController.ts +++ b/src/vs/editor/contrib/suggest/suggestController.ts @@ -121,7 +121,7 @@ export class SuggestController implements IEditorContribution { this.editor = editor; this.model = new SuggestModel(this.editor, editorWorker); - this.widget = new IdleValue(() => { + this.widget = this._toDispose.add(new IdleValue(() => { const widget = this._instantiationService.createInstance(SuggestWidget, this.editor); @@ -165,14 +165,27 @@ export class SuggestController implements IEditorContribution { })); this._toDispose.add(toDisposable(() => makesTextEdit.reset())); + this._toDispose.add(widget.onDetailsKeyDown(e => { + // cmd + c on macOS, ctrl + c on Win / Linux + if ( + e.toKeybinding().equals(new SimpleKeybinding(true, false, false, false, KeyCode.KEY_C)) || + (platform.isMacintosh && e.toKeybinding().equals(new SimpleKeybinding(false, false, false, true, KeyCode.KEY_C))) + ) { + e.stopPropagation(); + return; + } + if (!e.toKeybinding().isModifierKey()) { + this.editor.focus(); + } + })); return widget; - }); + })); - this._alternatives = new IdleValue(() => { + this._alternatives = this._toDispose.add(new IdleValue(() => { return this._toDispose.add(new SuggestAlternatives(this.editor, this._contextKeyService)); - }); + })); this._toDispose.add(_instantiationService.createInstance(WordContextKey, editor)); @@ -198,21 +211,6 @@ export class SuggestController implements IEditorContribution { } })); - this._toDispose.add(this.widget.getValue().onDetailsKeyDown(e => { - // cmd + c on macOS, ctrl + c on Win / Linux - if ( - e.toKeybinding().equals(new SimpleKeybinding(true, false, false, false, KeyCode.KEY_C)) || - (platform.isMacintosh && e.toKeybinding().equals(new SimpleKeybinding(false, false, false, true, KeyCode.KEY_C))) - ) { - e.stopPropagation(); - return; - } - - if (!e.toKeybinding().isModifierKey()) { - this.editor.focus(); - } - })); - // Manage the acceptSuggestionsOnEnter context key let acceptSuggestionsOnEnter = SuggestContext.AcceptSuggestionsOnEnter.bindTo(_contextKeyService); let updateFromConfig = () => { diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index fcd0fd41b9..09f1461740 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -22,7 +22,6 @@ import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition, IEditorMouseEvent } from 'vs/editor/browser/editorBrowser'; import { Context as SuggestContext, CompletionItem } from './suggest'; import { CompletionModel } from './completionModel'; -import { alert } from 'vs/base/browser/ui/aria/aria'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { attachListStyler } from 'vs/platform/theme/common/styler'; import { IThemeService, ITheme, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; @@ -89,6 +88,10 @@ function canExpandCompletionItem(item: CompletionItem | null) { return (suggestion.detail && suggestion.detail !== suggestion.label); } +function getAriaId(index: number): string { + return `suggest-aria-id:${index}`; +} + class Renderer implements IListRenderer { constructor( @@ -133,6 +136,7 @@ class Renderer implements IListRenderer const options = this.editor.getOptions(); const fontInfo = options.get(EditorOption.fontInfo); const fontFamily = fontInfo.fontFamily; + const fontFeatureSettings = fontInfo.fontFeatureSettings; const fontSize = options.get(EditorOption.suggestFontSize) || fontInfo.fontSize; const lineHeight = options.get(EditorOption.suggestLineHeight) || fontInfo.lineHeight; const fontWeight = fontInfo.fontWeight; @@ -142,6 +146,7 @@ class Renderer implements IListRenderer data.root.style.fontSize = fontSizePx; data.root.style.fontWeight = fontWeight; main.style.fontFamily = fontFamily; + main.style.fontFeatureSettings = fontFeatureSettings; main.style.lineHeight = lineHeightPx; data.icon.style.height = lineHeightPx; data.icon.style.width = lineHeightPx; @@ -158,10 +163,11 @@ class Renderer implements IListRenderer return data; } - renderElement(element: CompletionItem, _index: number, templateData: ISuggestionTemplateData): void { + renderElement(element: CompletionItem, index: number, templateData: ISuggestionTemplateData): void { const data = templateData; const suggestion = (element).completion; + data.root.id = getAriaId(index); data.icon.className = 'icon ' + completionKindToCssClass(suggestion.kind); data.colorspan.style.backgroundColor = ''; @@ -250,7 +256,6 @@ class SuggestionDetails { private header: HTMLElement; private type: HTMLElement; private docs: HTMLElement; - private ariaLabel: string | null; private readonly disposables: DisposableStore; private renderDisposeable?: IDisposable; private borderWidth: number = 1; @@ -279,7 +284,6 @@ class SuggestionDetails { this.type = append(this.header, $('p.type')); this.docs = append(this.body, $('p.docs')); - this.ariaLabel = null; this.configureFont(); @@ -318,7 +322,6 @@ class SuggestionDetails { this.type.textContent = ''; this.docs.textContent = ''; addClass(this.el, 'no-docs'); - this.ariaLabel = null; return; } removeClass(this.el, 'no-docs'); @@ -358,15 +361,6 @@ class SuggestionDetails { this.body.scrollTop = 0; this.scrollbar.scanDomNode(); - - this.ariaLabel = strings.format( - '{0}{1}', - detail || '', - documentation ? (typeof documentation === 'string' ? documentation : documentation.value) : ''); - } - - getAriaLabel() { - return this.ariaLabel; } scrollDown(much = 8): void { @@ -409,6 +403,7 @@ class SuggestionDetails { this.el.style.fontSize = fontSizePx; this.el.style.fontWeight = fontWeight; + this.el.style.fontFeatureSettings = fontInfo.fontFeatureSettings; this.type.style.fontFamily = fontFamily; this.close.style.height = lineHeightPx; this.close.style.width = lineHeightPx; @@ -521,7 +516,22 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate false }, - mouseSupport: false + mouseSupport: false, + accessibilityProvider: { + getAriaLabel: (item: CompletionItem) => { + if (item.isResolved && this.expandDocsSettingFromStorage()) { + const { documentation, detail } = item.completion; + const docs = strings.format( + '{0}{1}', + detail || '', + documentation ? (typeof documentation === 'string' ? documentation : documentation.value) : ''); + + return nls.localize('ariaCurrenttSuggestionReadDetails', "Item {0}, docs: {1}", item.completion.label, docs); + } else { + return item.completion.label; + } + } + } }); this.toDispose.add(attachListStyler(this.list, themeService, { @@ -610,25 +620,6 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegateEditorOptions.wrappingIndent).defaultValue = WrappingIndent.None; -(EditorOptions.glyphMargin).defaultValue = false; -(EditorOptions.autoIndent).defaultValue = 'advanced'; -(EditorOptions.overviewRulerLanes).defaultValue = 2; +EditorOptions.wrappingIndent.defaultValue = WrappingIndent.None; +EditorOptions.glyphMargin.defaultValue = false; +EditorOptions.autoIndent.defaultValue = EditorAutoIndentStrategy.Advanced; +EditorOptions.overviewRulerLanes.defaultValue = 2; const api = createMonacoBaseAPI(); api.editor = createMonacoEditorAPI(); diff --git a/src/vs/editor/standalone/browser/colorizer.ts b/src/vs/editor/standalone/browser/colorizer.ts index e0ff2588ff..b46188579e 100644 --- a/src/vs/editor/standalone/browser/colorizer.ts +++ b/src/vs/editor/standalone/browser/colorizer.ts @@ -125,6 +125,7 @@ export class Colorizer { [], tabSize, 0, + 0, -1, 'none', false, @@ -193,6 +194,7 @@ function _fakeColorize(lines: string[], tabSize: number): string { [], tabSize, 0, + 0, -1, 'none', false, @@ -230,6 +232,7 @@ function _actualColorize(lines: string[], tabSize: number, tokenizationSupport: [], tabSize, 0, + 0, -1, 'none', false, diff --git a/src/vs/editor/standalone/browser/quickOpen/editorQuickOpen.ts b/src/vs/editor/standalone/browser/quickOpen/editorQuickOpen.ts index 5356ce6053..37b874b8f4 100644 --- a/src/vs/editor/standalone/browser/quickOpen/editorQuickOpen.ts +++ b/src/vs/editor/standalone/browser/quickOpen/editorQuickOpen.ts @@ -10,7 +10,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, IActionOptions, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; -import * as editorCommon from 'vs/editor/common/editorCommon'; +import { IEditorContribution, ScrollType, IEditor } from 'vs/editor/common/editorCommon'; import { IModelDeltaDecoration } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { QuickOpenEditorWidget } from 'vs/editor/standalone/browser/quickOpen/quickOpenEditorWidget'; @@ -22,7 +22,7 @@ export interface IQuickOpenControllerOpts { getAutoFocus(searchValue: string): IAutoFocus; } -export class QuickOpenController implements editorCommon.IEditorContribution, IDecorator { +export class QuickOpenController implements IEditorContribution, IDecorator { public static readonly ID = 'editor.controller.quickOpenController'; @@ -61,7 +61,7 @@ export class QuickOpenController implements editorCommon.IEditorContribution, ID // Restore selection if canceled if (canceled && this.lastKnownEditorSelection) { this.editor.setSelection(this.lastKnownEditorSelection); - this.editor.revealRangeInCenterIfOutsideViewport(this.lastKnownEditorSelection, editorCommon.ScrollType.Smooth); + this.editor.revealRangeInCenterIfOutsideViewport(this.lastKnownEditorSelection, ScrollType.Smooth); } this.lastKnownEditorSelection = null; @@ -165,7 +165,7 @@ export abstract class BaseEditorQuickOpenAction extends EditorAction { } export interface IDecorator { - decorateLine(range: Range, editor: editorCommon.IEditor): void; + decorateLine(range: Range, editor: IEditor): void; clearDecorations(): void; } diff --git a/src/vs/editor/standalone/browser/quickOpen/gotoLine.ts b/src/vs/editor/standalone/browser/quickOpen/gotoLine.ts index 0a7e0414bb..0fcd25dd3f 100644 --- a/src/vs/editor/standalone/browser/quickOpen/gotoLine.ts +++ b/src/vs/editor/standalone/browser/quickOpen/gotoLine.ts @@ -12,7 +12,7 @@ import { ICodeEditor, IDiffEditor, isCodeEditor } from 'vs/editor/browser/editor import { ServicesAccessor, registerEditorAction } from 'vs/editor/browser/editorExtensions'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import * as editorCommon from 'vs/editor/common/editorCommon'; +import { IEditor, ScrollType } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ITextModel } from 'vs/editor/common/model'; import { BaseEditorQuickOpenAction, IDecorator } from 'vs/editor/standalone/browser/quickOpen/editorQuickOpen'; @@ -28,9 +28,9 @@ interface ParseResult { export class GotoLineEntry extends QuickOpenEntry { private readonly parseResult: ParseResult; private readonly decorator: IDecorator; - private readonly editor: editorCommon.IEditor; + private readonly editor: IEditor; - constructor(line: string, editor: editorCommon.IEditor, decorator: IDecorator) { + constructor(line: string, editor: IEditor, decorator: IDecorator) { super(); this.editor = editor; @@ -108,7 +108,7 @@ export class GotoLineEntry extends QuickOpenEntry { // Apply selection and focus const range = this.toSelection(); (this.editor).setSelection(range); - (this.editor).revealRangeInCenter(range, editorCommon.ScrollType.Smooth); + (this.editor).revealRangeInCenter(range, ScrollType.Smooth); this.editor.focus(); return true; @@ -124,7 +124,7 @@ export class GotoLineEntry extends QuickOpenEntry { // Select Line Position const range = this.toSelection(); - this.editor.revealRangeInCenter(range, editorCommon.ScrollType.Smooth); + this.editor.revealRangeInCenter(range, ScrollType.Smooth); // Decorate if possible this.decorator.decorateLine(range, this.editor); diff --git a/src/vs/editor/standalone/browser/simpleServices.ts b/src/vs/editor/standalone/browser/simpleServices.ts index d2fec79f28..2345d6f56f 100644 --- a/src/vs/editor/standalone/browser/simpleServices.ts +++ b/src/vs/editor/standalone/browser/simpleServices.ts @@ -18,9 +18,9 @@ import { isDiffEditorConfigurationKey, isEditorConfigurationKey } from 'vs/edito import { EditOperation } from 'vs/editor/common/core/editOperation'; import { IPosition, Position as Pos } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import * as editorCommon from 'vs/editor/common/editorCommon'; +import { IEditor } from 'vs/editor/common/editorCommon'; import { ITextModel, ITextSnapshot } from 'vs/editor/common/model'; -import { TextEdit, WorkspaceEdit, isResourceTextEdit } from 'vs/editor/common/modes'; +import { TextEdit, WorkspaceEdit, WorkspaceTextEdit } from 'vs/editor/common/modes'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IResolvedTextEditorModel, ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService'; import { ITextResourceConfigurationService, ITextResourcePropertiesService, ITextResourceConfigurationChangeEvent } from 'vs/editor/common/services/textResourceConfigurationService'; @@ -80,13 +80,17 @@ export class SimpleModel implements IResolvedTextEditorModel { public dispose(): void { this._onDispose.fire(); } + + public isResolved(): boolean { + return true; + } } export interface IOpenEditorDelegate { (url: string): boolean; } -function withTypedEditor(widget: editorCommon.IEditor, codeEditorCallback: (editor: ICodeEditor) => T, diffEditorCallback: (editor: IDiffEditor) => T): T { +function withTypedEditor(widget: IEditor, codeEditorCallback: (editor: ICodeEditor) => T, diffEditorCallback: (editor: IDiffEditor) => T): T { if (isCodeEditor(widget)) { // Single Editor return codeEditorCallback(widget); @@ -100,13 +104,13 @@ export class SimpleEditorModelResolverService implements ITextModelService { public _serviceBrand: undefined; private readonly modelService: IModelService | undefined; - private editor?: editorCommon.IEditor; + private editor?: IEditor; constructor(modelService: IModelService | undefined) { this.modelService = modelService; } - public setEditor(editor: editorCommon.IEditor): void { + public setEditor(editor: IEditor): void { this.editor = editor; } @@ -631,13 +635,17 @@ export class SimpleBulkEditService implements IBulkEditService { // } + setPreviewHandler(): IDisposable { + return Disposable.None; + } + apply(workspaceEdit: WorkspaceEdit, options?: IBulkEditOptions): Promise { let edits = new Map(); if (workspaceEdit.edits) { for (let edit of workspaceEdit.edits) { - if (!isResourceTextEdit(edit)) { + if (!WorkspaceTextEdit.is(edit)) { return Promise.reject(new Error('bad edit - only text edits are supported')); } let model = this._modelService.getModel(edit.resource); diff --git a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts index ae60a6231d..2626561784 100644 --- a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts @@ -77,10 +77,58 @@ export interface IActionDescriptor { run(editor: ICodeEditor): void | Promise; } +/** + * Options which apply for all editors. + */ +export interface IGlobalEditorOptions { + /** + * The number of spaces a tab is equal to. + * This setting is overridden based on the file contents when `detectIndentation` is on. + * Defaults to 4. + */ + tabSize?: number; + /** + * Insert spaces when pressing `Tab`. + * This setting is overridden based on the file contents when detectIndentation` is on. + * Defaults to true. + */ + insertSpaces?: boolean; + /** + * Controls whether `tabSize` and `insertSpaces` will be automatically detected when a file is opened based on the file contents. + * Defaults to true. + */ + detectIndentation?: boolean; + /** + * Remove trailing auto inserted whitespace. + * Defaults to true. + */ + trimAutoWhitespace?: boolean; + /** + * Special handling for large files to disable certain memory intensive features. + * Defaults to true. + */ + largeFileOptimizations?: boolean; + /** + * Controls whether completions should be computed based on words in the document. + * Defaults to true. + */ + wordBasedSuggestions?: boolean; + /** + * Keep peek editors open even when double clicking their content or when hitting `Escape`. + * Defaults to false. + */ + stablePeek?: boolean; + /** + * Lines above this length will not be tokenized for performance reasons. + * Defaults to 20000. + */ + maxTokenizationLineLength?: number; +} + /** * The options to create an editor. */ -export interface IStandaloneEditorConstructionOptions extends IEditorConstructionOptions { +export interface IStandaloneEditorConstructionOptions extends IEditorConstructionOptions, IGlobalEditorOptions { /** * The initial model associated with this code editor. */ @@ -125,6 +173,7 @@ export interface IDiffEditorConstructionOptions extends IDiffEditorOptions { } export interface IStandaloneCodeEditor extends ICodeEditor { + updateOptions(newOptions: IEditorOptions & IGlobalEditorOptions): void; addCommand(keybinding: number, handler: ICommandHandler, context?: string): string | null; createContextKey(key: string, defaultValue: T): IContextKey; addAction(descriptor: IActionDescriptor): IDisposable; @@ -337,7 +386,7 @@ export class StandaloneEditor extends StandaloneCodeEditor implements IStandalon super.dispose(); } - public updateOptions(newOptions: IEditorOptions): void { + public updateOptions(newOptions: IEditorOptions & IGlobalEditorOptions): void { applyConfigurationValues(this._configurationService, newOptions, false); super.updateOptions(newOptions); } diff --git a/src/vs/editor/standalone/browser/standaloneEditor.ts b/src/vs/editor/standalone/browser/standaloneEditor.ts index ea739d2ae9..5bf328fdc1 100644 --- a/src/vs/editor/standalone/browser/standaloneEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneEditor.ts @@ -10,10 +10,10 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { OpenerService } from 'vs/editor/browser/services/openerService'; import { DiffNavigator, IDiffNavigator } from 'vs/editor/browser/widget/diffNavigator'; -import { ConfigurationChangedEvent } from 'vs/editor/common/config/editorOptions'; +import { EditorOptions, ConfigurationChangedEvent } from 'vs/editor/common/config/editorOptions'; import { BareFontInfo, FontInfo } from 'vs/editor/common/config/fontInfo'; import { Token } from 'vs/editor/common/core/token'; -import * as editorCommon from 'vs/editor/common/editorCommon'; +import { IEditor, EditorType } from 'vs/editor/common/editorCommon'; import { FindMatch, ITextModel, TextModelResolvedOptions } from 'vs/editor/common/model'; import * as modes from 'vs/editor/common/modes'; import { NULL_STATE, nullTokenize } from 'vs/editor/common/modes/nullMode'; @@ -42,7 +42,7 @@ import { IEditorProgressService } from 'vs/platform/progress/common/progress'; type Omit = Pick>; -function withAllStandaloneServices(domElement: HTMLElement, override: IEditorOverrideServices, callback: (services: DynamicStandaloneServices) => T): T { +function withAllStandaloneServices(domElement: HTMLElement, override: IEditorOverrideServices, callback: (services: DynamicStandaloneServices) => T): T { let services = new DynamicStandaloneServices(domElement, override); let simpleEditorModelResolverService: SimpleEditorModelResolverService | null = null; @@ -346,19 +346,26 @@ export function createMonacoEditorAPI(): typeof monaco.editor { remeasureFonts: remeasureFonts, // enums - ScrollbarVisibility: standaloneEnums.ScrollbarVisibility, - OverviewRulerLane: standaloneEnums.OverviewRulerLane, - MinimapPosition: standaloneEnums.MinimapPosition, - EndOfLinePreference: standaloneEnums.EndOfLinePreference, - DefaultEndOfLine: standaloneEnums.DefaultEndOfLine, - EndOfLineSequence: standaloneEnums.EndOfLineSequence, - TrackedRangeStickiness: standaloneEnums.TrackedRangeStickiness, - CursorChangeReason: standaloneEnums.CursorChangeReason, - MouseTargetType: standaloneEnums.MouseTargetType, + AccessibilitySupport: standaloneEnums.AccessibilitySupport, ContentWidgetPositionPreference: standaloneEnums.ContentWidgetPositionPreference, + CursorChangeReason: standaloneEnums.CursorChangeReason, + DefaultEndOfLine: standaloneEnums.DefaultEndOfLine, + EditorAutoIndentStrategy: standaloneEnums.EditorAutoIndentStrategy, + EditorOption: standaloneEnums.EditorOption, + EndOfLinePreference: standaloneEnums.EndOfLinePreference, + EndOfLineSequence: standaloneEnums.EndOfLineSequence, + MinimapPosition: standaloneEnums.MinimapPosition, + MouseTargetType: standaloneEnums.MouseTargetType, OverlayWidgetPositionPreference: standaloneEnums.OverlayWidgetPositionPreference, + OverviewRulerLane: standaloneEnums.OverviewRulerLane, + RenderLineNumbersType: standaloneEnums.RenderLineNumbersType, RenderMinimap: standaloneEnums.RenderMinimap, + ScrollbarVisibility: standaloneEnums.ScrollbarVisibility, ScrollType: standaloneEnums.ScrollType, + TextEditorCursorBlinkingStyle: standaloneEnums.TextEditorCursorBlinkingStyle, + TextEditorCursorStyle: standaloneEnums.TextEditorCursorStyle, + TrackedRangeStickiness: standaloneEnums.TrackedRangeStickiness, + WrappingIndent: standaloneEnums.WrappingIndent, // classes ConfigurationChangedEvent: ConfigurationChangedEvent, @@ -368,7 +375,8 @@ export function createMonacoEditorAPI(): typeof monaco.editor { FindMatch: FindMatch, // vars - EditorType: editorCommon.EditorType, + EditorType: EditorType, + EditorOptions: EditorOptions }; } diff --git a/src/vs/editor/test/browser/commands/shiftCommand.test.ts b/src/vs/editor/test/browser/commands/shiftCommand.test.ts index b709eb0a75..34bc3a2893 100644 --- a/src/vs/editor/test/browser/commands/shiftCommand.test.ts +++ b/src/vs/editor/test/browser/commands/shiftCommand.test.ts @@ -22,7 +22,8 @@ import { EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions' export function createSingleEditOp(text: string, positionLineNumber: number, positionColumn: number, selectionLineNumber: number = positionLineNumber, selectionColumn: number = positionColumn): IIdentifiedSingleEditOperation { return { range: new Range(selectionLineNumber, selectionColumn, positionLineNumber, positionColumn), - text: text + text: text, + forceMoveMarkers: false }; } diff --git a/src/vs/editor/test/browser/commands/sideEditing.test.ts b/src/vs/editor/test/browser/commands/sideEditing.test.ts index 4d1044e4ed..f6358d5732 100644 --- a/src/vs/editor/test/browser/commands/sideEditing.test.ts +++ b/src/vs/editor/test/browser/commands/sideEditing.test.ts @@ -14,6 +14,7 @@ import { TextModel } from 'vs/editor/common/model/textModel'; 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'; +import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/monospaceLineBreaksComputer'; function testCommand(lines: string[], selections: Selection[], edits: IIdentifiedSingleEditOperation[], expectedLines: string[], expectedSelections: Selection[]): void { withTestCodeEditor(lines, {}, (editor, cursor) => { @@ -200,7 +201,8 @@ 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 config = new TestConfiguration({}); - const viewModel = new ViewModel(0, config, model, null!); + const monospaceLineBreaksComputerFactory = MonospaceLineBreaksComputerFactory.create(config.options); + const viewModel = new ViewModel(0, config, model, monospaceLineBreaksComputerFactory, monospaceLineBreaksComputerFactory, null!); const cursor = new Cursor(config, model, viewModel); cursor.setSelections('tests', [selection]); diff --git a/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts b/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts index bec93faf1b..5309f1afb2 100644 --- a/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts +++ b/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts @@ -28,7 +28,8 @@ function createInsertDeleteSingleEditOp(text: string | null, positionLineNumber: export function createSingleEditOp(text: string | null, positionLineNumber: number, positionColumn: number, selectionLineNumber: number = positionLineNumber, selectionColumn: number = positionColumn): IIdentifiedSingleEditOperation { return { range: new Range(selectionLineNumber, selectionColumn, positionLineNumber, positionColumn), - text: text + text: text, + forceMoveMarkers: false }; } diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index 7dc1ba1213..0d39d6a8ce 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -12,7 +12,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 { TokenizationResult2 } from 'vs/editor/common/core/token'; -import { Handler, ICommand, ICursorStateComputerData, IEditOperationBuilder } from 'vs/editor/common/editorCommon'; +import { Handler, ICommand, ICursorStateComputerData, IEditOperationBuilder, IConfiguration } from 'vs/editor/common/editorCommon'; import { EndOfLinePreference, EndOfLineSequence, ITextModel } from 'vs/editor/common/model'; import { TextModel } from 'vs/editor/common/model/textModel'; import { IState, ITokenizationSupport, LanguageIdentifier, TokenizationRegistry } from 'vs/editor/common/modes'; @@ -25,6 +25,7 @@ import { IRelaxedTextModelCreationOptions, createTextModel } from 'vs/editor/tes import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; import { TestConfiguration } from 'vs/editor/test/common/mocks/testConfiguration'; import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/javascriptOnEnterRules'; +import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/monospaceLineBreaksComputer'; const H = Handler; @@ -130,6 +131,11 @@ function assertCursor(cursor: Cursor, what: Position | Selection | Selection[]): assert.deepEqual(actual, expected); } +function createViewModel(configuration: IConfiguration, model: ITextModel): ViewModel { + const monospaceLineBreaksComputerFactory = MonospaceLineBreaksComputerFactory.create(configuration.options); + return new ViewModel(0, configuration, model, monospaceLineBreaksComputerFactory, monospaceLineBreaksComputerFactory, null!); +} + suite('Editor Controller - Cursor', () => { const LINE1 = ' \tMy First Line\t '; const LINE2 = '\tMy Second Line'; @@ -152,7 +158,7 @@ suite('Editor Controller - Cursor', () => { thisModel = createTextModel(text); thisConfiguration = new TestConfiguration({}); - thisViewModel = new ViewModel(0, thisConfiguration, thisModel, null!); + thisViewModel = createViewModel(thisConfiguration, thisModel); thisCursor = new Cursor(thisConfiguration, thisModel, thisViewModel); }); @@ -776,7 +782,7 @@ suite('Editor Controller - Cursor', () => { 'var newer = require("gulp-newer");', ].join('\n')); const config = new TestConfiguration({}); - const viewModel = new ViewModel(0, config, model, null!); + const viewModel = createViewModel(config, model); const cursor = new Cursor(config, model, viewModel); moveTo(cursor, 1, 4, false); @@ -816,7 +822,7 @@ suite('Editor Controller - Cursor', () => { '', ].join('\n')); const config = new TestConfiguration({}); - const viewModel = new ViewModel(0, config, model, null!); + const viewModel = createViewModel(config, model); const cursor = new Cursor(config, model, viewModel); moveTo(cursor, 10, 10, false); @@ -880,7 +886,7 @@ suite('Editor Controller - Cursor', () => { '', ].join('\n')); const config = new TestConfiguration({}); - const viewModel = new ViewModel(0, config, model, null!); + const viewModel = createViewModel(config, model); const cursor = new Cursor(config, model, viewModel); moveTo(cursor, 10, 10, false); @@ -929,7 +935,7 @@ suite('Editor Controller - Cursor', () => { 'var newer = require("gulp-newer");', ].join('\n')); const config = new TestConfiguration({}); - const viewModel = new ViewModel(0, config, model, null!); + const viewModel = createViewModel(config, model); const cursor = new Cursor(config, model, viewModel); moveTo(cursor, 1, 4, false); @@ -2074,7 +2080,7 @@ suite('Editor Controller - Regression tests', () => { wordWrap: 'wordWrapColumn', wordWrapColumn: 100 }); - const viewModel = new ViewModel(0, config, model, null!); + const viewModel = createViewModel(config, model); const cursor = new Cursor(config, model, viewModel); moveTo(cursor, 1, 43, false); @@ -2273,6 +2279,64 @@ suite('Editor Controller - Regression tests', () => { model.dispose(); }); + + test('issue #85712: Paste line moves cursor to start of current line rather than start of next line', () => { + let model = createTextModel( + [ + 'abc123', + '' + ].join('\n') + ); + + withTestCodeEditor(null, { model: model }, (editor, cursor) => { + editor.setSelections([ + new Selection(2, 1, 2, 1) + ]); + cursorCommand(cursor, H.Paste, { text: 'something\n', pasteOnNewLine: true }); + assert.equal(model.getValue(), [ + 'abc123', + 'something', + '' + ].join('\n')); + assertCursor(cursor, new Position(3, 1)); + }); + + model.dispose(); + }); + + test('issue #84897: Left delete behavior in some languages is changed', () => { + let model = createTextModel( + [ + 'สวัสดี' + ].join('\n') + ); + + withTestCodeEditor(null, { model: model }, (editor, cursor) => { + editor.setSelections([ + new Selection(1, 7, 1, 7) + ]); + + CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); + assert.equal(model.getValue(EndOfLinePreference.LF), 'สวัสด'); + + CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); + assert.equal(model.getValue(EndOfLinePreference.LF), 'สวัส'); + + CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); + assert.equal(model.getValue(EndOfLinePreference.LF), 'สวั'); + + CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); + assert.equal(model.getValue(EndOfLinePreference.LF), 'สว'); + + CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); + assert.equal(model.getValue(EndOfLinePreference.LF), 'ส'); + + CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); + assert.equal(model.getValue(EndOfLinePreference.LF), ''); + }); + + model.dispose(); + }); }); suite('Editor Controller - Cursor Configuration', () => { @@ -2630,7 +2694,7 @@ suite('Editor Controller - Cursor Configuration', () => { '', ' }', ].join('\n')); - assertCursor(cursor, new Position(4, 1)); + assertCursor(cursor, new Position(5, 1)); }); model.dispose(); @@ -3834,7 +3898,7 @@ function usingCursor(opts: ICursorOpts, callback: (model: TextModel, cursor: Cur let model = createTextModel(opts.text.join('\n'), opts.modelOpts, opts.languageIdentifier); model.forceTokenization(model.getLineCount()); let config = new TestConfiguration(opts.editorOpts || {}); - let viewModel = new ViewModel(0, config, model, null!); + let viewModel = createViewModel(config, model); let cursor = new Cursor(config, model, viewModel); callback(model, cursor); diff --git a/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts b/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts index 98332860d7..9499089ce2 100644 --- a/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts +++ b/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts @@ -13,6 +13,7 @@ import { Selection } from 'vs/editor/common/core/selection'; 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'; suite('Cursor move command test', () => { @@ -32,7 +33,8 @@ suite('Cursor move command test', () => { thisModel = TextModel.createFromString(text); thisConfiguration = new TestConfiguration({}); - thisViewModel = new ViewModel(0, thisConfiguration, thisModel, null!); + const monospaceLineBreaksComputerFactory = MonospaceLineBreaksComputerFactory.create(thisConfiguration.options); + thisViewModel = new ViewModel(0, thisConfiguration, thisModel, monospaceLineBreaksComputerFactory, monospaceLineBreaksComputerFactory, null!); thisCursor = new Cursor(thisConfiguration, thisModel, thisViewModel); }); diff --git a/src/vs/editor/test/browser/testCodeEditor.ts b/src/vs/editor/test/browser/testCodeEditor.ts index 61546080ed..fa7b67dbcd 100644 --- a/src/vs/editor/test/browser/testCodeEditor.ts +++ b/src/vs/editor/test/browser/testCodeEditor.ts @@ -3,13 +3,13 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as editorBrowser from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { View } from 'vs/editor/browser/view/viewImpl'; import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; import * as editorOptions from 'vs/editor/common/config/editorOptions'; import { Cursor } from 'vs/editor/common/controller/cursor'; -import * as editorCommon from 'vs/editor/common/editorCommon'; +import { IConfiguration, IEditorContribution } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; import { TextModel } from 'vs/editor/common/model/textModel'; import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; @@ -26,10 +26,10 @@ import { TestNotificationService } from 'vs/platform/notification/test/common/te import { IThemeService } from 'vs/platform/theme/common/themeService'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; -export class TestCodeEditor extends CodeEditorWidget implements editorBrowser.ICodeEditor { +export class TestCodeEditor extends CodeEditorWidget implements ICodeEditor { //#region testing overrides - protected _createConfiguration(options: editorOptions.IEditorConstructionOptions): editorCommon.IConfiguration { + protected _createConfiguration(options: editorOptions.IEditorConstructionOptions): IConfiguration { return new TestConfiguration(options); } protected _createView(viewModel: ViewModel, cursor: Cursor): [View, boolean] { @@ -42,7 +42,7 @@ export class TestCodeEditor extends CodeEditorWidget implements editorBrowser.IC public getCursor(): Cursor | undefined { return this._modelData ? this._modelData.cursor : undefined; } - public registerAndInstantiateContribution(id: string, ctor: any): T { + public registerAndInstantiateContribution(id: string, ctor: any): T { let r = this._instantiationService.createInstance(ctor, this); this._contributions[id] = r; return r; diff --git a/src/vs/editor/test/browser/testCommand.ts b/src/vs/editor/test/browser/testCommand.ts index a9779c3c49..08b1fd60ce 100644 --- a/src/vs/editor/test/browser/testCommand.ts +++ b/src/vs/editor/test/browser/testCommand.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; -import * as editorCommon from 'vs/editor/common/editorCommon'; +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 { LanguageIdentifier } from 'vs/editor/common/modes'; @@ -16,7 +16,7 @@ export function testCommand( lines: string[], languageIdentifier: LanguageIdentifier | null, selection: Selection, - commandFactory: (selection: Selection) => editorCommon.ICommand, + commandFactory: (selection: Selection) => ICommand, expectedLines: string[], expectedSelection: Selection, forceTokenization?: boolean @@ -33,7 +33,7 @@ export function testCommand( cursor.setSelections('tests', [selection]); - cursor.trigger('tests', editorCommon.Handler.ExecuteCommand, commandFactory(cursor.getSelection())); + cursor.trigger('tests', Handler.ExecuteCommand, commandFactory(cursor.getSelection())); assert.deepEqual(model.getLinesContent(), expectedLines); @@ -47,20 +47,22 @@ export function testCommand( /** * Extract edit operations if command `command` were to execute on model `model` */ -export function getEditOperation(model: ITextModel, command: editorCommon.ICommand): IIdentifiedSingleEditOperation[] { +export function getEditOperation(model: ITextModel, command: ICommand): IIdentifiedSingleEditOperation[] { let operations: IIdentifiedSingleEditOperation[] = []; - let editOperationBuilder: editorCommon.IEditOperationBuilder = { - addEditOperation: (range: Range, text: string) => { + let editOperationBuilder: IEditOperationBuilder = { + addEditOperation: (range: Range, text: string, forceMoveMarkers: boolean = false) => { operations.push({ range: range, - text: text + text: text, + forceMoveMarkers: forceMoveMarkers }); }, - addTrackedEditOperation: (range: Range, text: string) => { + addTrackedEditOperation: (range: Range, text: string, forceMoveMarkers: boolean = false) => { operations.push({ range: range, - text: text + text: text, + forceMoveMarkers: forceMoveMarkers }); }, diff --git a/src/vs/editor/test/common/model/textModel.test.ts b/src/vs/editor/test/common/model/textModel.test.ts index 07dd91eee3..255953c175 100644 --- a/src/vs/editor/test/common/model/textModel.test.ts +++ b/src/vs/editor/test/common/model/textModel.test.ts @@ -743,87 +743,6 @@ suite('Editor Model - TextModel', () => { assert.deepEqual(m.validatePosition(new Position(2, 1.5)), new Position(2, 1), 'h'); }); - function assertValidatePosition(m: TextModel, lineNumber: number, column: number, expectedColumn: number): void { - const input = new Position(lineNumber, column); - const actual = m.validatePosition(input); - const expected = new Position(lineNumber, expectedColumn); - assert.deepEqual(actual, expected, `validatePosition for ${input}, got ${actual}, expected ${expected}`); - } - - function assertValidateRange(m: TextModel, input: Range, expected: Range): void { - const actual = m.validateRange(input); - assert.deepEqual(actual, expected, `validateRange for ${input}, got ${actual}, expected ${expected}`); - } - - test('grapheme breaking', () => { - const m = TextModel.createFromString([ - 'abcabc', - 'ãããããã', - '辻󠄀辻󠄀辻󠄀', - 'புபுபு', - ].join('\n')); - - assertValidatePosition(m, 2, 1, 1); - assertValidatePosition(m, 2, 2, 1); - assertValidatePosition(m, 2, 3, 3); - assertValidatePosition(m, 2, 4, 3); - assertValidatePosition(m, 2, 5, 5); - assertValidatePosition(m, 2, 6, 5); - assertValidatePosition(m, 2, 7, 7); - assertValidatePosition(m, 2, 8, 7); - assertValidatePosition(m, 2, 9, 9); - assertValidatePosition(m, 2, 10, 9); - assertValidatePosition(m, 2, 11, 11); - assertValidatePosition(m, 2, 12, 11); - assertValidatePosition(m, 2, 13, 13); - assertValidatePosition(m, 2, 14, 13); - - assertValidatePosition(m, 3, 1, 1); - assertValidatePosition(m, 3, 2, 1); - assertValidatePosition(m, 3, 3, 1); - assertValidatePosition(m, 3, 4, 4); - assertValidatePosition(m, 3, 5, 4); - assertValidatePosition(m, 3, 6, 4); - assertValidatePosition(m, 3, 7, 7); - assertValidatePosition(m, 3, 8, 7); - assertValidatePosition(m, 3, 9, 7); - assertValidatePosition(m, 3, 10, 10); - - assertValidatePosition(m, 4, 1, 1); - assertValidatePosition(m, 4, 2, 1); - assertValidatePosition(m, 4, 3, 3); - assertValidatePosition(m, 4, 4, 3); - assertValidatePosition(m, 4, 5, 5); - assertValidatePosition(m, 4, 6, 5); - assertValidatePosition(m, 4, 7, 7); - - assertValidateRange(m, new Range(2, 1, 2, 1), new Range(2, 1, 2, 1)); - assertValidateRange(m, new Range(2, 1, 2, 2), new Range(2, 1, 2, 3)); - assertValidateRange(m, new Range(2, 1, 2, 3), new Range(2, 1, 2, 3)); - assertValidateRange(m, new Range(2, 1, 2, 4), new Range(2, 1, 2, 5)); - assertValidateRange(m, new Range(2, 1, 2, 5), new Range(2, 1, 2, 5)); - assertValidateRange(m, new Range(2, 2, 2, 2), new Range(2, 1, 2, 1)); - assertValidateRange(m, new Range(2, 2, 2, 3), new Range(2, 1, 2, 3)); - assertValidateRange(m, new Range(2, 2, 2, 4), new Range(2, 1, 2, 5)); - assertValidateRange(m, new Range(2, 2, 2, 5), new Range(2, 1, 2, 5)); - - assertValidateRange(m, new Range(3, 1, 3, 1), new Range(3, 1, 3, 1)); - assertValidateRange(m, new Range(3, 1, 3, 2), new Range(3, 1, 3, 4)); - assertValidateRange(m, new Range(3, 1, 3, 3), new Range(3, 1, 3, 4)); - assertValidateRange(m, new Range(3, 1, 3, 4), new Range(3, 1, 3, 4)); - assertValidateRange(m, new Range(3, 1, 3, 5), new Range(3, 1, 3, 7)); - assertValidateRange(m, new Range(3, 1, 3, 6), new Range(3, 1, 3, 7)); - assertValidateRange(m, new Range(3, 1, 3, 7), new Range(3, 1, 3, 7)); - assertValidateRange(m, new Range(3, 2, 3, 2), new Range(3, 1, 3, 1)); - assertValidateRange(m, new Range(3, 2, 3, 3), new Range(3, 1, 3, 4)); - assertValidateRange(m, new Range(3, 2, 3, 4), new Range(3, 1, 3, 4)); - assertValidateRange(m, new Range(3, 2, 3, 5), new Range(3, 1, 3, 7)); - assertValidateRange(m, new Range(3, 2, 3, 6), new Range(3, 1, 3, 7)); - assertValidateRange(m, new Range(3, 2, 3, 7), new Range(3, 1, 3, 7)); - - m.dispose(); - }); - test('issue #71480: validateRange handle floats', () => { let m = TextModel.createFromString('line one\nline two'); diff --git a/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts b/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts index 846aa15f89..32fdd4a48f 100644 --- a/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts +++ b/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts @@ -112,19 +112,15 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 0, - glyphMarginHeight: 800, lineNumbersLeft: 0, lineNumbersWidth: 0, - lineNumbersHeight: 800, decorationsLeft: 0, decorationsWidth: 10, - decorationsHeight: 800, contentLeft: 10, contentWidth: 990, - contentHeight: 800, renderMinimap: RenderMinimap.None, minimapLeft: 0, @@ -170,19 +166,15 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 0, - glyphMarginHeight: 800, lineNumbersLeft: 0, lineNumbersWidth: 0, - lineNumbersHeight: 800, decorationsLeft: 0, decorationsWidth: 10, - decorationsHeight: 800, contentLeft: 10, contentWidth: 990, - contentHeight: 800, renderMinimap: RenderMinimap.None, minimapLeft: 0, @@ -228,19 +220,15 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 0, - glyphMarginHeight: 800, lineNumbersLeft: 0, lineNumbersWidth: 0, - lineNumbersHeight: 800, decorationsLeft: 0, decorationsWidth: 10, - decorationsHeight: 800, contentLeft: 10, contentWidth: 890, - contentHeight: 800, renderMinimap: RenderMinimap.None, minimapLeft: 0, @@ -286,19 +274,15 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 0, - glyphMarginHeight: 900, lineNumbersLeft: 0, lineNumbersWidth: 0, - lineNumbersHeight: 900, decorationsLeft: 0, decorationsWidth: 10, - decorationsHeight: 900, contentLeft: 10, contentWidth: 890, - contentHeight: 900, renderMinimap: RenderMinimap.None, minimapLeft: 0, @@ -344,19 +328,15 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 0, - glyphMarginHeight: 900, lineNumbersLeft: 0, lineNumbersWidth: 0, - lineNumbersHeight: 900, decorationsLeft: 0, decorationsWidth: 10, - decorationsHeight: 900, contentLeft: 10, contentWidth: 890, - contentHeight: 900, renderMinimap: RenderMinimap.None, minimapLeft: 0, @@ -402,19 +382,15 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 0, - glyphMarginHeight: 900, lineNumbersLeft: 0, lineNumbersWidth: 50, - lineNumbersHeight: 900, decorationsLeft: 50, decorationsWidth: 10, - decorationsHeight: 900, contentLeft: 60, contentWidth: 840, - contentHeight: 900, renderMinimap: RenderMinimap.None, minimapLeft: 0, @@ -460,19 +436,15 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 0, - glyphMarginHeight: 900, lineNumbersLeft: 0, lineNumbersWidth: 50, - lineNumbersHeight: 900, decorationsLeft: 50, decorationsWidth: 10, - decorationsHeight: 900, contentLeft: 60, contentWidth: 840, - contentHeight: 900, renderMinimap: RenderMinimap.None, minimapLeft: 0, @@ -518,19 +490,15 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 0, - glyphMarginHeight: 900, lineNumbersLeft: 0, lineNumbersWidth: 60, - lineNumbersHeight: 900, decorationsLeft: 60, decorationsWidth: 10, - decorationsHeight: 900, contentLeft: 70, contentWidth: 830, - contentHeight: 900, renderMinimap: RenderMinimap.None, minimapLeft: 0, @@ -576,19 +544,15 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 0, - glyphMarginHeight: 900, lineNumbersLeft: 0, lineNumbersWidth: 30, - lineNumbersHeight: 900, decorationsLeft: 30, decorationsWidth: 10, - decorationsHeight: 900, contentLeft: 40, contentWidth: 860, - contentHeight: 900, renderMinimap: RenderMinimap.None, minimapLeft: 0, @@ -634,19 +598,15 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 0, - glyphMarginHeight: 900, lineNumbersLeft: 0, lineNumbersWidth: 30, - lineNumbersHeight: 900, decorationsLeft: 30, decorationsWidth: 10, - decorationsHeight: 900, contentLeft: 40, contentWidth: 860, - contentHeight: 900, renderMinimap: RenderMinimap.None, minimapLeft: 0, @@ -692,19 +652,15 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 0, - glyphMarginHeight: 800, lineNumbersLeft: 0, lineNumbersWidth: 0, - lineNumbersHeight: 800, decorationsLeft: 0, decorationsWidth: 10, - decorationsHeight: 800, contentLeft: 10, contentWidth: 893, - contentHeight: 800, renderMinimap: RenderMinimap.Text, minimapLeft: 903, @@ -750,19 +706,15 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 0, - glyphMarginHeight: 800, lineNumbersLeft: 0, lineNumbersWidth: 0, - lineNumbersHeight: 800, decorationsLeft: 0, decorationsWidth: 10, - decorationsHeight: 800, contentLeft: 10, contentWidth: 893, - contentHeight: 800, renderMinimap: RenderMinimap.Text, minimapLeft: 903, @@ -808,19 +760,15 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 0, - glyphMarginHeight: 800, lineNumbersLeft: 0, lineNumbersWidth: 0, - lineNumbersHeight: 800, decorationsLeft: 0, decorationsWidth: 10, - decorationsHeight: 800, contentLeft: 10, contentWidth: 935, - contentHeight: 800, renderMinimap: RenderMinimap.Text, minimapLeft: 945, @@ -866,19 +814,15 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 55, glyphMarginWidth: 0, - glyphMarginHeight: 800, lineNumbersLeft: 55, lineNumbersWidth: 0, - lineNumbersHeight: 800, decorationsLeft: 55, decorationsWidth: 10, - decorationsHeight: 800, contentLeft: 65, contentWidth: 935, - contentHeight: 800, renderMinimap: RenderMinimap.Text, minimapLeft: 0, @@ -924,19 +868,15 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { glyphMarginLeft: 0, glyphMarginWidth: 30, - glyphMarginHeight: 422, lineNumbersLeft: 30, lineNumbersWidth: 36, - lineNumbersHeight: 422, decorationsLeft: 66, decorationsWidth: 26, - decorationsHeight: 422, contentLeft: 92, contentWidth: 1018, - contentHeight: 422, renderMinimap: RenderMinimap.Text, minimapLeft: 1096, diff --git a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts index a33a5ac526..ba78fbb9e9 100644 --- a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts +++ b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts @@ -38,6 +38,7 @@ suite('viewLineRenderer.renderLine', () => { [], tabSize, 0, + 0, -1, 'none', false, @@ -88,6 +89,7 @@ suite('viewLineRenderer.renderLine', () => { [], tabSize, 0, + 0, -1, 'none', false, @@ -140,6 +142,7 @@ suite('viewLineRenderer.renderLine', () => { ]), [], 4, + 0, 10, 6, 'boundary', @@ -232,6 +235,7 @@ suite('viewLineRenderer.renderLine', () => { lineParts, [], 4, + 0, 10, -1, 'boundary', @@ -295,6 +299,7 @@ suite('viewLineRenderer.renderLine', () => { lineParts, [], 4, + 0, 10, -1, 'none', @@ -358,6 +363,7 @@ suite('viewLineRenderer.renderLine', () => { lineParts, [], 4, + 0, 10, -1, 'none', @@ -398,6 +404,7 @@ suite('viewLineRenderer.renderLine', () => { lineParts, [], 4, + 0, 10, -1, 'none', @@ -429,6 +436,7 @@ suite('viewLineRenderer.renderLine', () => { lineParts, [], 4, + 0, 10, -1, 'none', @@ -530,6 +538,7 @@ suite('viewLineRenderer.renderLine', () => { lineParts, [], 4, + 0, 10, -1, 'none', @@ -569,6 +578,7 @@ suite('viewLineRenderer.renderLine', () => { lineParts, [], 4, + 0, 10, -1, 'none', @@ -599,6 +609,7 @@ suite('viewLineRenderer.renderLine', () => { lineParts, [], 4, + 0, 10, -1, 'none', @@ -646,6 +657,7 @@ suite('viewLineRenderer.renderLine', () => { lineParts, [], 4, + 0, 10, -1, 'none', @@ -728,6 +740,7 @@ suite('viewLineRenderer.renderLine 2', () => { createViewLineTokens(tokens), [], 4, + 0, 10, -1, renderWhitespace, @@ -754,6 +767,7 @@ suite('viewLineRenderer.renderLine 2', () => { createViewLineTokens([createPart(21, 3)]), [new LineDecoration(1, 22, 'link', InlineDecorationType.Regular)], 4, + 0, 10, -1, 'none', @@ -794,6 +808,7 @@ suite('viewLineRenderer.renderLine 2', () => { new LineDecoration(13, 51, 'detected-link', InlineDecorationType.Regular) ], 4, + 0, 10, -1, 'none', @@ -1209,6 +1224,7 @@ suite('viewLineRenderer.renderLine 2', () => { new LineDecoration(2, 8, 'c', InlineDecorationType.Regular), ], 4, + 0, 10, -1, 'none', @@ -1250,6 +1266,7 @@ suite('viewLineRenderer.renderLine 2', () => { createViewLineTokens([createPart(4, 3)]), [new LineDecoration(1, 2, 'before', InlineDecorationType.Before)], 4, + 0, 10, -1, 'all', @@ -1283,6 +1300,7 @@ suite('viewLineRenderer.renderLine 2', () => { createViewLineTokens([createPart(4, 3)]), [new LineDecoration(2, 3, 'before', InlineDecorationType.Before)], 4, + 0, 10, -1, 'all', @@ -1317,6 +1335,7 @@ suite('viewLineRenderer.renderLine 2', () => { createViewLineTokens([createPart(0, 3)]), [new LineDecoration(1, 2, 'before', InlineDecorationType.Before)], 4, + 0, 10, -1, 'all', @@ -1347,6 +1366,7 @@ suite('viewLineRenderer.renderLine 2', () => { createViewLineTokens([createPart(7, 3)]), [new LineDecoration(7, 8, 'inline-folded', InlineDecorationType.After)], 2, + 0, 10, 10000, 'none', @@ -1365,7 +1385,7 @@ suite('viewLineRenderer.renderLine 2', () => { assert.deepEqual(actual.html, expected); }); - test('issue #37401: Allow both before and after decorations on empty line', () => { + test('issue #37401 #40127: Allow both before and after decorations on empty line', () => { let actual = renderViewLine(new RenderLineInput( true, @@ -1381,6 +1401,7 @@ suite('viewLineRenderer.renderLine 2', () => { new LineDecoration(0, 1, 'after', InlineDecorationType.After), ], 2, + 0, 10, 10000, 'none', @@ -1391,7 +1412,8 @@ suite('viewLineRenderer.renderLine 2', () => { let expected = [ '', - '', + '', + '', '' ].join(''); @@ -1414,6 +1436,7 @@ suite('viewLineRenderer.renderLine 2', () => { new LineDecoration(3, 3, 'ced-TextEditorDecorationType2-5e9b9b3f-4 ced-TextEditorDecorationType2-4', InlineDecorationType.After), ], 4, + 0, 10, 10000, 'none', @@ -1445,6 +1468,7 @@ suite('viewLineRenderer.renderLine 2', () => { createViewLineTokens([createPart(15, 3)]), [], 4, + 0, 10, 10000, 'none', @@ -1475,6 +1499,7 @@ suite('viewLineRenderer.renderLine 2', () => { createViewLineTokens([createPart(15, 3)]), [], 4, + 0, 10, 10000, 'all', @@ -1511,6 +1536,7 @@ suite('viewLineRenderer.renderLine 2', () => { createViewLineTokens([createPart(53, 3)]), [], 4, + 0, 10, 10000, 'none', @@ -1541,6 +1567,7 @@ suite('viewLineRenderer.renderLine 2', () => { createViewLineTokens([createPart(100, 3)]), [], 4, + 0, 10, 10000, 'none', @@ -1573,6 +1600,7 @@ suite('viewLineRenderer.renderLine 2', () => { createViewLineTokens([createPart(105, 3)]), [], 4, + 0, 10, 10000, 'none', @@ -1604,6 +1632,7 @@ suite('viewLineRenderer.renderLine 2', () => { createViewLineTokens([createPart(59, 3)]), [], 4, + 0, 10, 10000, 'boundary', @@ -1633,6 +1662,7 @@ suite('viewLineRenderer.renderLine 2', () => { createViewLineTokens([createPart(194, 3)]), [], 4, + 0, 10, 10000, 'none', @@ -1666,6 +1696,7 @@ suite('viewLineRenderer.renderLine 2', () => { createViewLineTokens([createPart(194, 3)]), [], 4, + 0, 10, 10000, 'none', @@ -1695,6 +1726,7 @@ suite('viewLineRenderer.renderLine 2', () => { createViewLineTokens(parts), [], tabSize, + 0, 10, -1, 'none', diff --git a/src/vs/editor/test/common/viewModel/characterHardWrappingLineMapper.test.ts b/src/vs/editor/test/common/viewModel/characterHardWrappingLineMapper.test.ts deleted file mode 100644 index 0ee91485ba..0000000000 --- a/src/vs/editor/test/common/viewModel/characterHardWrappingLineMapper.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; -import { WrappingIndent } from 'vs/editor/common/config/editorOptions'; -import { CharacterHardWrappingLineMapperFactory } from 'vs/editor/common/viewModel/characterHardWrappingLineMapper'; -import { ILineMapperFactory, ILineMapping } from 'vs/editor/common/viewModel/splitLinesCollection'; - -function assertLineMapping(factory: ILineMapperFactory, tabSize: number, breakAfter: number, annotatedText: string, wrappingIndent = WrappingIndent.None): ILineMapping | null { - // Create version of `annotatedText` with line break markers removed - let rawText = ''; - let currentLineIndex = 0; - let lineIndices: number[] = []; - for (let i = 0, len = annotatedText.length; i < len; i++) { - if (annotatedText.charAt(i) === '|') { - currentLineIndex++; - } else { - rawText += annotatedText.charAt(i); - lineIndices[rawText.length - 1] = currentLineIndex; - } - } - - const mapper = factory.createLineMapping(rawText, tabSize, breakAfter, 2, wrappingIndent); - - // Insert line break markers again, according to algorithm - let actualAnnotatedText = ''; - if (mapper) { - let previousLineIndex = 0; - for (let i = 0, len = rawText.length; i < len; i++) { - let r = mapper.getOutputPositionOfInputOffset(i); - if (previousLineIndex !== r.outputLineIndex) { - previousLineIndex = r.outputLineIndex; - actualAnnotatedText += '|'; - } - actualAnnotatedText += rawText.charAt(i); - } - } else { - // No wrapping - actualAnnotatedText = rawText; - } - - assert.equal(actualAnnotatedText, annotatedText); - - return mapper; -} - -suite('Editor ViewModel - CharacterHardWrappingLineMapper', () => { - test('CharacterHardWrappingLineMapper', () => { - - let factory = new CharacterHardWrappingLineMapperFactory('(', ')', '.'); - - // Empty string - assertLineMapping(factory, 4, 5, ''); - - // No wrapping if not necessary - assertLineMapping(factory, 4, 5, 'aaa'); - assertLineMapping(factory, 4, 5, 'aaaaa'); - assertLineMapping(factory, 4, -1, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); - - // Acts like hard wrapping if no char found - assertLineMapping(factory, 4, 5, 'aaaaa|a'); - - // Honors obtrusive wrapping character - assertLineMapping(factory, 4, 5, 'aaaaa|.'); - assertLineMapping(factory, 4, 5, 'aaaaa|a.|aaa.|aa'); - assertLineMapping(factory, 4, 5, 'aaaaa|a..|aaa.|aa'); - assertLineMapping(factory, 4, 5, 'aaaaa|a...|aaa.|aa'); - assertLineMapping(factory, 4, 5, 'aaaaa|a....|aaa.|aa'); - - // Honors tabs when computing wrapping position - assertLineMapping(factory, 4, 5, '\t'); - assertLineMapping(factory, 4, 5, '\ta|aa'); - assertLineMapping(factory, 4, 5, '\ta|\ta|a'); - assertLineMapping(factory, 4, 5, 'aa\ta'); - assertLineMapping(factory, 4, 5, 'aa\ta|a'); - - // Honors wrapping before characters (& gives it priority) - assertLineMapping(factory, 4, 5, 'aaa.|aa'); - assertLineMapping(factory, 4, 5, 'aaa|(.aa'); - - // Honors wrapping after characters (& gives it priority) - assertLineMapping(factory, 4, 5, 'aaa))|).aaa'); - assertLineMapping(factory, 4, 5, 'aaa))|)|.aaaa'); - assertLineMapping(factory, 4, 5, 'aaa)|()|.aaa'); - assertLineMapping(factory, 4, 5, 'aaa(|()|.aaa'); - assertLineMapping(factory, 4, 5, 'aa.(|()|.aaa'); - assertLineMapping(factory, 4, 5, 'aa.|(.)|.aaa'); - }); - - test('CharacterHardWrappingLineMapper - CJK and Kinsoku Shori', () => { - let factory = new CharacterHardWrappingLineMapperFactory('(', ')', '.'); - assertLineMapping(factory, 4, 5, 'aa \u5b89|\u5b89'); - assertLineMapping(factory, 4, 5, '\u3042 \u5b89|\u5b89'); - assertLineMapping(factory, 4, 5, '\u3042\u3042|\u5b89\u5b89'); - assertLineMapping(factory, 4, 5, 'aa |\u5b89)\u5b89|\u5b89'); - assertLineMapping(factory, 4, 5, 'aa \u3042|\u5b89\u3042)|\u5b89'); - assertLineMapping(factory, 4, 5, 'aa |(\u5b89aa|\u5b89'); - }); - - test('CharacterHardWrappingLineMapper - WrappingIndent.Same', () => { - let factory = new CharacterHardWrappingLineMapperFactory('', ' ', ''); - assertLineMapping(factory, 4, 38, ' *123456789012345678901234567890123456|7890', WrappingIndent.Same); - }); - - test('issue #16332: Scroll bar overlaying on top of text', () => { - let factory = new CharacterHardWrappingLineMapperFactory('', ' ', ''); - assertLineMapping(factory, 4, 24, 'a/ very/long/line/of/tex|t/that/expands/beyon|d/your/typical/line/|of/code/', WrappingIndent.Indent); - }); - - test('issue #35162: wrappingIndent not consistently working', () => { - let factory = new CharacterHardWrappingLineMapperFactory('', ' ', ''); - let mapper = assertLineMapping(factory, 4, 24, ' t h i s |i s |a l |o n |g l |i n |e', WrappingIndent.Indent); - assert.equal(mapper!.getWrappedLinesIndent(), ' \t'); - }); - - test('issue #75494: surrogate pairs', () => { - let factory = new CharacterHardWrappingLineMapperFactory('', ' ', ''); - assertLineMapping(factory, 4, 49, '🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇|👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬', WrappingIndent.Same); - }); - - test('CharacterHardWrappingLineMapper - WrappingIndent.DeepIndent', () => { - let factory = new CharacterHardWrappingLineMapperFactory('', ' ', ''); - let mapper = assertLineMapping(factory, 4, 26, ' W e A r e T e s t |i n g D e |e p I n d |e n t a t |i o n', WrappingIndent.DeepIndent); - assert.equal(mapper!.getWrappedLinesIndent(), ' \t\t'); - }); -}); diff --git a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts new file mode 100644 index 0000000000..93c94a33bc --- /dev/null +++ b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts @@ -0,0 +1,256 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as assert from 'assert'; +import { WrappingIndent, EditorOptions } from 'vs/editor/common/config/editorOptions'; +import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/monospaceLineBreaksComputer'; +import { ILineBreaksComputerFactory, LineBreakData } from 'vs/editor/common/viewModel/splitLinesCollection'; +import { FontInfo } from 'vs/editor/common/config/fontInfo'; + +function parseAnnotatedText(annotatedText: string): { text: string; indices: number[]; } { + let text = ''; + let currentLineIndex = 0; + let indices: number[] = []; + for (let i = 0, len = annotatedText.length; i < len; i++) { + if (annotatedText.charAt(i) === '|') { + currentLineIndex++; + } else { + text += annotatedText.charAt(i); + indices[text.length - 1] = currentLineIndex; + } + } + return { text: text, indices: indices }; +} + +function toAnnotatedText(text: string, lineBreakData: LineBreakData | null): string { + // Insert line break markers again, according to algorithm + let actualAnnotatedText = ''; + if (lineBreakData) { + let previousLineIndex = 0; + for (let i = 0, len = text.length; i < len; i++) { + let r = LineBreakData.getOutputPositionOfInputOffset(lineBreakData.breakOffsets, i); + if (previousLineIndex !== r.outputLineIndex) { + previousLineIndex = r.outputLineIndex; + actualAnnotatedText += '|'; + } + actualAnnotatedText += text.charAt(i); + } + } else { + // No wrapping + actualAnnotatedText = text; + } + return actualAnnotatedText; +} + +function getLineBreakData(factory: ILineBreaksComputerFactory, tabSize: number, breakAfter: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent, text: string, previousLineBreakData: LineBreakData | null): LineBreakData | null { + const fontInfo = new FontInfo({ + zoomLevel: 0, + fontFamily: 'testFontFamily', + fontWeight: 'normal', + fontSize: 14, + fontFeatureSettings: '', + lineHeight: 19, + letterSpacing: 0, + isMonospace: true, + typicalHalfwidthCharacterWidth: 7, + typicalFullwidthCharacterWidth: 14, + canUseHalfwidthRightwardsArrow: true, + spaceWidth: 7, + maxDigitWidth: 7 + }, false); + const lineBreaksComputer = factory.createLineBreaksComputer(fontInfo, tabSize, breakAfter, wrappingIndent); + const previousLineBreakDataClone = previousLineBreakData ? new LineBreakData(previousLineBreakData.breakOffsets.slice(0), previousLineBreakData.breakOffsetsVisibleColumn.slice(0), previousLineBreakData.wrappedTextIndentLength) : null; + lineBreaksComputer.addRequest(text, previousLineBreakDataClone); + return lineBreaksComputer.finalize()[0]; +} + +function assertLineBreaks(factory: ILineBreaksComputerFactory, tabSize: number, breakAfter: number, annotatedText: string, wrappingIndent = WrappingIndent.None): LineBreakData | null { + // Create version of `annotatedText` with line break markers removed + const text = parseAnnotatedText(annotatedText).text; + const lineBreakData = getLineBreakData(factory, tabSize, breakAfter, 2, wrappingIndent, text, null); + const actualAnnotatedText = toAnnotatedText(text, lineBreakData); + + assert.equal(actualAnnotatedText, annotatedText); + + return lineBreakData; +} + +suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { + test('MonospaceLineBreaksComputer', () => { + + let factory = new MonospaceLineBreaksComputerFactory('(', '\t).'); + + // Empty string + assertLineBreaks(factory, 4, 5, ''); + + // No wrapping if not necessary + assertLineBreaks(factory, 4, 5, 'aaa'); + assertLineBreaks(factory, 4, 5, 'aaaaa'); + assertLineBreaks(factory, 4, -1, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); + + // Acts like hard wrapping if no char found + assertLineBreaks(factory, 4, 5, 'aaaaa|a'); + + // Honors wrapping character + assertLineBreaks(factory, 4, 5, 'aaaaa|.'); + assertLineBreaks(factory, 4, 5, 'aaaaa|a.|aaa.|aa'); + assertLineBreaks(factory, 4, 5, 'aaaaa|a..|aaa.|aa'); + assertLineBreaks(factory, 4, 5, 'aaaaa|a...|aaa.|aa'); + assertLineBreaks(factory, 4, 5, 'aaaaa|a....|aaa.|aa'); + + // Honors tabs when computing wrapping position + assertLineBreaks(factory, 4, 5, '\t'); + assertLineBreaks(factory, 4, 5, '\t|aaa'); + assertLineBreaks(factory, 4, 5, '\t|a\t|aa'); + assertLineBreaks(factory, 4, 5, 'aa\ta'); + assertLineBreaks(factory, 4, 5, 'aa\t|aa'); + + // Honors wrapping before characters (& gives it priority) + assertLineBreaks(factory, 4, 5, 'aaa.|aa'); + assertLineBreaks(factory, 4, 5, 'aaa(.|aa'); + + // Honors wrapping after characters (& gives it priority) + assertLineBreaks(factory, 4, 5, 'aaa))|).aaa'); + assertLineBreaks(factory, 4, 5, 'aaa))|).|aaaa'); + assertLineBreaks(factory, 4, 5, 'aaa)|().|aaa'); + assertLineBreaks(factory, 4, 5, 'aaa(|().|aaa'); + assertLineBreaks(factory, 4, 5, 'aa.(|().|aaa'); + assertLineBreaks(factory, 4, 5, 'aa.(.|).aaa'); + }); + + function assertIncrementalLineBreaks(factory: ILineBreaksComputerFactory, text: string, tabSize: number, breakAfter1: number, annotatedText1: string, breakAfter2: number, annotatedText2: string, wrappingIndent = WrappingIndent.None): void { + // sanity check the test + assert.equal(text, parseAnnotatedText(annotatedText1).text); + assert.equal(text, parseAnnotatedText(annotatedText2).text); + + // check that the direct mapping is ok for 1 + const directLineBreakData1 = getLineBreakData(factory, tabSize, breakAfter1, 2, wrappingIndent, text, null); + assert.equal(toAnnotatedText(text, directLineBreakData1), annotatedText1); + + // check that the direct mapping is ok for 2 + const directLineBreakData2 = getLineBreakData(factory, tabSize, breakAfter2, 2, wrappingIndent, text, null); + assert.equal(toAnnotatedText(text, directLineBreakData2), annotatedText2); + + // check that going from 1 to 2 is ok + const lineBreakData2from1 = getLineBreakData(factory, tabSize, breakAfter2, 2, wrappingIndent, text, directLineBreakData1); + assert.equal(toAnnotatedText(text, lineBreakData2from1), annotatedText2); + assert.deepEqual(lineBreakData2from1, directLineBreakData2); + + // check that going from 2 to 1 is ok + const lineBreakData1from2 = getLineBreakData(factory, tabSize, breakAfter1, 2, wrappingIndent, text, directLineBreakData2); + assert.equal(toAnnotatedText(text, lineBreakData1from2), annotatedText1); + assert.deepEqual(lineBreakData1from2, directLineBreakData1); + } + + test('MonospaceLineBreaksComputer incremental 1', () => { + + let factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue); + + assertIncrementalLineBreaks( + factory, 'just some text and more', 4, + 10, 'just some |text and |more', + 15, 'just some text |and more' + ); + + assertIncrementalLineBreaks( + factory, 'Cu scripserit suscipiantur eos, in affert pericula contentiones sed, cetero sanctus et pro. Ius vidit magna regione te, sit ei elaboraret liberavisse. Mundi verear eu mea, eam vero scriptorem in, vix in menandri assueverit. Natum definiebas cu vim. Vim doming vocibus efficiantur id. In indoctum deseruisse voluptatum vim, ad debitis verterem sed.', 4, + 47, 'Cu scripserit suscipiantur eos, in affert |pericula contentiones sed, cetero sanctus et |pro. Ius vidit magna regione te, sit ei |elaboraret liberavisse. Mundi verear eu mea, |eam vero scriptorem in, vix in menandri |assueverit. Natum definiebas cu vim. Vim |doming vocibus efficiantur id. In indoctum |deseruisse voluptatum vim, ad debitis verterem |sed.', + 142, 'Cu scripserit suscipiantur eos, in affert pericula contentiones sed, cetero sanctus et pro. Ius vidit magna regione te, sit ei elaboraret |liberavisse. Mundi verear eu mea, eam vero scriptorem in, vix in menandri assueverit. Natum definiebas cu vim. Vim doming vocibus efficiantur |id. In indoctum deseruisse voluptatum vim, ad debitis verterem sed.', + ); + + assertIncrementalLineBreaks( + factory, 'An his legere persecuti, oblique delicata efficiantur ex vix, vel at graecis officiis maluisset. Et per impedit voluptua, usu discere maiorum at. Ut assum ornatus temporibus vis, an sea melius pericula. Ea dicunt oblique phaedrum nam, eu duo movet nobis. His melius facilis eu, vim malorum temporibus ne. Nec no sale regione, meliore civibus placerat id eam. Mea alii fabulas definitionem te, agam volutpat ad vis, et per bonorum nonumes repudiandae.', 4, + 57, 'An his legere persecuti, oblique delicata efficiantur ex |vix, vel at graecis officiis maluisset. Et per impedit |voluptua, usu discere maiorum at. Ut assum ornatus |temporibus vis, an sea melius pericula. Ea dicunt |oblique phaedrum nam, eu duo movet nobis. His melius |facilis eu, vim malorum temporibus ne. Nec no sale |regione, meliore civibus placerat id eam. Mea alii |fabulas definitionem te, agam volutpat ad vis, et per |bonorum nonumes repudiandae.', + 58, 'An his legere persecuti, oblique delicata efficiantur ex |vix, vel at graecis officiis maluisset. Et per impedit |voluptua, usu discere maiorum at. Ut assum ornatus |temporibus vis, an sea melius pericula. Ea dicunt oblique |phaedrum nam, eu duo movet nobis. His melius facilis eu, |vim malorum temporibus ne. Nec no sale regione, meliore |civibus placerat id eam. Mea alii fabulas definitionem |te, agam volutpat ad vis, et per bonorum nonumes |repudiandae.' + ); + + assertIncrementalLineBreaks( + factory, '\t\t"owner": "vscode",', 4, + 14, '\t\t"owner|": |"vscod|e",', + 16, '\t\t"owner":| |"vscode"|,', + WrappingIndent.Same + ); + + assertIncrementalLineBreaks( + factory, '🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇&👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬', 4, + 51, '🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇&|👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬', + 50, '🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇|&|👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬', + WrappingIndent.Same + ); + + assertIncrementalLineBreaks( + factory, '🐇👬&🌞🌖', 4, + 5, '🐇👬&|🌞🌖', + 4, '🐇👬|&|🌞🌖', + WrappingIndent.Same + ); + + assertIncrementalLineBreaks( + factory, '\t\tfunc(\'🌞🏇🍼🌞🏇🍼🐇&👬🌖🌞👬🌖🌞🏇🍼🐇👬\', WrappingIndent.Same);', 4, + 26, '\t\tfunc|(\'🌞🏇🍼🌞🏇🍼🐇&|👬🌖🌞👬🌖🌞🏇🍼🐇|👬\', |WrappingIndent.|Same);', + 27, '\t\tfunc|(\'🌞🏇🍼🌞🏇🍼🐇&|👬🌖🌞👬🌖🌞🏇🍼🐇|👬\', |WrappingIndent.|Same);', + WrappingIndent.Same + ); + + assertIncrementalLineBreaks( + factory, 'factory, "xtxtfunc(x"🌞🏇🍼🌞🏇🍼🐇&👬🌖🌞👬🌖🌞🏇🍼🐇👬x"', 4, + 16, 'factory, |"xtxtfunc|(x"🌞🏇🍼🌞🏇🍼|🐇&|👬🌖🌞👬🌖🌞🏇🍼|🐇👬x"', + 17, 'factory, |"xtxtfunc|(x"🌞🏇🍼🌞🏇🍼🐇|&👬🌖🌞👬🌖🌞🏇🍼|🐇👬x"', + WrappingIndent.Same + ); + }); + + + test('MonospaceLineBreaksComputer - CJK and Kinsoku Shori', () => { + let factory = new MonospaceLineBreaksComputerFactory('(', '\t)'); + assertLineBreaks(factory, 4, 5, 'aa \u5b89|\u5b89'); + assertLineBreaks(factory, 4, 5, '\u3042 \u5b89|\u5b89'); + assertLineBreaks(factory, 4, 5, '\u3042\u3042|\u5b89\u5b89'); + assertLineBreaks(factory, 4, 5, 'aa |\u5b89)\u5b89|\u5b89'); + assertLineBreaks(factory, 4, 5, 'aa \u3042|\u5b89\u3042)|\u5b89'); + assertLineBreaks(factory, 4, 5, 'aa |(\u5b89aa|\u5b89'); + }); + + test('MonospaceLineBreaksComputer - WrappingIndent.Same', () => { + let factory = new MonospaceLineBreaksComputerFactory('', '\t '); + assertLineBreaks(factory, 4, 38, ' *123456789012345678901234567890123456|7890', WrappingIndent.Same); + }); + + test('issue #16332: Scroll bar overlaying on top of text', () => { + let factory = new MonospaceLineBreaksComputerFactory('', '\t '); + assertLineBreaks(factory, 4, 24, 'a/ very/long/line/of/tex|t/that/expands/beyon|d/your/typical/line/|of/code/', WrappingIndent.Indent); + }); + + test('issue #35162: wrappingIndent not consistently working', () => { + let factory = new MonospaceLineBreaksComputerFactory('', '\t '); + let mapper = assertLineBreaks(factory, 4, 24, ' t h i s |i s |a l |o n |g l |i n |e', WrappingIndent.Indent); + assert.equal(mapper!.wrappedTextIndentLength, ' '.length); + }); + + test('issue #75494: surrogate pairs', () => { + let factory = new MonospaceLineBreaksComputerFactory('\t', ' '); + assertLineBreaks(factory, 4, 49, '🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼|🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼|🐇👬', WrappingIndent.Same); + }); + + test('issue #75494: surrogate pairs overrun 1', () => { + const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue); + assertLineBreaks(factory, 4, 4, '🐇👬|&|🌞🌖', WrappingIndent.Same); + }); + + test('issue #75494: surrogate pairs overrun 2', () => { + const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue); + assertLineBreaks(factory, 4, 17, 'factory, |"xtxtfunc|(x"🌞🏇🍼🌞🏇🍼🐇|&👬🌖🌞👬🌖🌞🏇🍼|🐇👬x"', WrappingIndent.Same); + }); + + test('MonospaceLineBreaksComputer - WrappingIndent.DeepIndent', () => { + let factory = new MonospaceLineBreaksComputerFactory('', '\t '); + let mapper = assertLineBreaks(factory, 4, 26, ' W e A r e T e s t |i n g D e |e p I n d |e n t a t |i o n', WrappingIndent.DeepIndent); + assert.equal(mapper!.wrappedTextIndentLength, ' '.length); + }); + + test('issue #33366: Word wrap algorithm behaves differently around punctuation', () => { + const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue); + assertLineBreaks(factory, 4, 23, 'this is a line of |text, text that sits |on a line', WrappingIndent.Same); + }); +}); diff --git a/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts b/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts index 887f2e4833..74a5e05d64 100644 --- a/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts @@ -4,9 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { toUint32Array } from 'vs/base/common/uint'; +import { toUint32 } from 'vs/base/common/uint'; import { PrefixSumComputer, PrefixSumIndexOfResult } from 'vs/editor/common/viewModel/prefixSumComputer'; +function toUint32Array(arr: number[]): Uint32Array { + const len = arr.length; + const r = new Uint32Array(len); + for (let i = 0; i < len; i++) { + r[i] = toUint32(arr[i]); + } + return r; +} + suite('Editor ViewModel - PrefixSumComputer', () => { test('PrefixSumComputer', () => { diff --git a/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts b/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts index 5f5beed381..b687b934bc 100644 --- a/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts +++ b/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts @@ -9,23 +9,20 @@ import { IViewLineTokens } from 'vs/editor/common/core/lineTokens'; import { Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { TokenizationResult2 } from 'vs/editor/common/core/token'; -import { toUint32Array } from 'vs/base/common/uint'; import { EndOfLinePreference } from 'vs/editor/common/model'; 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 { CharacterHardWrappingLineMapperFactory, CharacterHardWrappingLineMapping } from 'vs/editor/common/viewModel/characterHardWrappingLineMapper'; -import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; -import { ILineMapping, ISimpleModel, SplitLine, SplitLinesCollection } from 'vs/editor/common/viewModel/splitLinesCollection'; +import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/monospaceLineBreaksComputer'; +import { LineBreakData, ISimpleModel, SplitLine, SplitLinesCollection } from 'vs/editor/common/viewModel/splitLinesCollection'; 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'; - suite('Editor ViewModel - SplitLinesCollection', () => { test('SplitLine', () => { let model1 = createModel('My First LineMy Second LineAnd another one'); - let line1 = createSplitLine([13, 14, 15], ''); + let line1 = createSplitLine([13, 14, 15], [13, 13 + 14, 13 + 14 + 15], 0); assert.equal(line1.getViewLineCount(), 3); assert.equal(line1.getViewLineContent(model1, 1, 0), 'My First Line'); @@ -54,38 +51,38 @@ suite('Editor ViewModel - SplitLinesCollection', () => { } model1 = createModel('My First LineMy Second LineAnd another one'); - line1 = createSplitLine([13, 14, 15], '\t'); + line1 = createSplitLine([13, 14, 15], [13, 13 + 14, 13 + 14 + 15], 4); assert.equal(line1.getViewLineCount(), 3); assert.equal(line1.getViewLineContent(model1, 1, 0), 'My First Line'); - assert.equal(line1.getViewLineContent(model1, 1, 1), '\tMy Second Line'); - assert.equal(line1.getViewLineContent(model1, 1, 2), '\tAnd another one'); + assert.equal(line1.getViewLineContent(model1, 1, 1), ' My Second Line'); + assert.equal(line1.getViewLineContent(model1, 1, 2), ' And another one'); assert.equal(line1.getViewLineMaxColumn(model1, 1, 0), 14); - assert.equal(line1.getViewLineMaxColumn(model1, 1, 1), 16); - assert.equal(line1.getViewLineMaxColumn(model1, 1, 2), 17); - for (let col = 1; col <= 14; col++) { - assert.equal(line1.getModelColumnOfViewPosition(0, col), col, 'getInputColumnOfOutputPosition(0, ' + col + ')'); - } - for (let col = 1; col <= 1; col++) { - assert.equal(line1.getModelColumnOfViewPosition(1, 1), 13 + col, 'getInputColumnOfOutputPosition(1, ' + col + ')'); - } - for (let col = 2; col <= 16; col++) { - assert.equal(line1.getModelColumnOfViewPosition(1, col), 13 + col - 1, 'getInputColumnOfOutputPosition(1, ' + col + ')'); - } - for (let col = 1; col <= 1; col++) { - assert.equal(line1.getModelColumnOfViewPosition(2, col), 13 + 14 + col, 'getInputColumnOfOutputPosition(2, ' + col + ')'); - } - for (let col = 2; col <= 17; col++) { - assert.equal(line1.getModelColumnOfViewPosition(2, col), 13 + 14 + col - 1, 'getInputColumnOfOutputPosition(2, ' + col + ')'); + assert.equal(line1.getViewLineMaxColumn(model1, 1, 1), 19); + assert.equal(line1.getViewLineMaxColumn(model1, 1, 2), 20); + + let actualViewColumnMapping: number[][] = []; + for (let lineIndex = 0; lineIndex < line1.getViewLineCount(); lineIndex++) { + let actualLineViewColumnMapping: number[] = []; + for (let col = 1; col <= line1.getViewLineMaxColumn(model1, 1, lineIndex); col++) { + actualLineViewColumnMapping.push(line1.getModelColumnOfViewPosition(lineIndex, col)); + } + actualViewColumnMapping.push(actualLineViewColumnMapping); } + assert.deepEqual(actualViewColumnMapping, [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], + [14, 14, 14, 14, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28], + [28, 28, 28, 28, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43], + ]); + for (let col = 1; col <= 13; col++) { - assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(0, col), 'getOutputPositionOfInputPosition(' + col + ')'); + assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(0, col), '6.getOutputPositionOfInputPosition(' + col + ')'); } for (let col = 1 + 13; col <= 14 + 13; col++) { - assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(1, 1 + col - 13), 'getOutputPositionOfInputPosition(' + col + ')'); + assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(1, 4 + col - 13), '7.getOutputPositionOfInputPosition(' + col + ')'); } for (let col = 1 + 13 + 14; col <= 15 + 14 + 13; col++) { - assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(2, 1 + col - 13 - 14), 'getOutputPositionOfInputPosition(' + col + ')'); + assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(2, 4 + col - 13 - 14), '8.getOutputPositionOfInputPosition(' + col + ')'); } }); @@ -95,14 +92,9 @@ suite('Editor ViewModel - SplitLinesCollection', () => { const fontInfo = config.options.get(EditorOption.fontInfo); const wordWrapBreakAfterCharacters = config.options.get(EditorOption.wordWrapBreakAfterCharacters); const wordWrapBreakBeforeCharacters = config.options.get(EditorOption.wordWrapBreakBeforeCharacters); - const wordWrapBreakObtrusiveCharacters = config.options.get(EditorOption.wordWrapBreakObtrusiveCharacters); const wrappingIndent = config.options.get(EditorOption.wrappingIndent); - const hardWrappingLineMapperFactory = new CharacterHardWrappingLineMapperFactory( - wordWrapBreakBeforeCharacters, - wordWrapBreakAfterCharacters, - wordWrapBreakObtrusiveCharacters - ); + const lineBreaksComputerFactory = new MonospaceLineBreaksComputerFactory(wordWrapBreakBeforeCharacters, wordWrapBreakAfterCharacters); const model = TextModel.createFromString([ 'int main() {', @@ -115,10 +107,12 @@ suite('Editor ViewModel - SplitLinesCollection', () => { const linesCollection = new SplitLinesCollection( model, - hardWrappingLineMapperFactory, + lineBreaksComputerFactory, + lineBreaksComputerFactory, + fontInfo, model.getOptions().tabSize, + 'monospace', wrappingInfo.wrappingColumn, - fontInfo.typicalFullwidthCharacterWidth / fontInfo.typicalHalfwidthCharacterWidth, wrappingIndent ); @@ -616,12 +610,12 @@ suite('SplitLinesCollection', () => { ] }, { - content: ' world");', - minColumn: 4, - maxColumn: 12, + content: ' world");', + minColumn: 13, + maxColumn: 21, tokens: [ - { endIndex: 9, value: 15 }, - { endIndex: 11, value: 16 }, + { endIndex: 18, value: 15 }, + { endIndex: 20, value: 16 }, ] }, { @@ -658,28 +652,28 @@ suite('SplitLinesCollection', () => { ] }, { - content: ' world, this is a ', - minColumn: 4, - maxColumn: 21, + content: ' world, this is a ', + minColumn: 13, + maxColumn: 30, tokens: [ - { endIndex: 20, value: 28 }, + { endIndex: 29, value: 28 }, ] }, { - content: ' somewhat longer ', - minColumn: 4, + content: ' somewhat longer ', + minColumn: 13, + maxColumn: 29, + tokens: [ + { endIndex: 28, value: 28 }, + ] + }, + { + content: ' line");', + minColumn: 13, maxColumn: 20, tokens: [ - { endIndex: 19, value: 28 }, - ] - }, - { - content: ' line");', - minColumn: 4, - maxColumn: 11, - tokens: [ - { endIndex: 8, value: 28 }, - { endIndex: 10, value: 29 }, + { endIndex: 17, value: 28 }, + { endIndex: 19, value: 29 }, ] }, { @@ -749,21 +743,18 @@ suite('SplitLinesCollection', () => { const fontInfo = configuration.options.get(EditorOption.fontInfo); const wordWrapBreakAfterCharacters = configuration.options.get(EditorOption.wordWrapBreakAfterCharacters); const wordWrapBreakBeforeCharacters = configuration.options.get(EditorOption.wordWrapBreakBeforeCharacters); - const wordWrapBreakObtrusiveCharacters = configuration.options.get(EditorOption.wordWrapBreakObtrusiveCharacters); const wrappingIndent = configuration.options.get(EditorOption.wrappingIndent); - const factory = new CharacterHardWrappingLineMapperFactory( - wordWrapBreakBeforeCharacters, - wordWrapBreakAfterCharacters, - wordWrapBreakObtrusiveCharacters - ); + const lineBreaksComputerFactory = new MonospaceLineBreaksComputerFactory(wordWrapBreakBeforeCharacters, wordWrapBreakAfterCharacters); const linesCollection = new SplitLinesCollection( model, - factory, + lineBreaksComputerFactory, + lineBreaksComputerFactory, + fontInfo, model.getOptions().tabSize, + 'monospace', wrappingInfo.wrappingColumn, - fontInfo.typicalFullwidthCharacterWidth / fontInfo.typicalHalfwidthCharacterWidth, wrappingIndent ); @@ -778,15 +769,16 @@ function pos(lineNumber: number, column: number): Position { return new Position(lineNumber, column); } -function createSplitLine(splitLengths: number[], wrappedLinesPrefix: string, isVisible: boolean = true): SplitLine { - return new SplitLine(createLineMapping(splitLengths, wrappedLinesPrefix), isVisible); +function createSplitLine(splitLengths: number[], breakingOffsetsVisibleColumn: number[], wrappedTextIndentWidth: number, isVisible: boolean = true): SplitLine { + return new SplitLine(createLineBreakData(splitLengths, breakingOffsetsVisibleColumn, wrappedTextIndentWidth), isVisible); } -function createLineMapping(breakingLengths: number[], wrappedLinesPrefix: string): ILineMapping { - return new CharacterHardWrappingLineMapping( - new PrefixSumComputer(toUint32Array(breakingLengths)), - wrappedLinesPrefix - ); +function createLineBreakData(breakingLengths: number[], breakingOffsetsVisibleColumn: number[], wrappedTextIndentWidth: number): LineBreakData { + let sums: number[] = []; + for (let i = 0; i < breakingLengths.length; i++) { + sums[i] = (i > 0 ? sums[i - 1] : 0) + breakingLengths[i]; + } + return new LineBreakData(sums, breakingOffsetsVisibleColumn, wrappedTextIndentWidth); } function createModel(text: string): ISimpleModel { diff --git a/src/vs/editor/test/common/viewModel/testViewModel.ts b/src/vs/editor/test/common/viewModel/testViewModel.ts index 5dd307e724..f6867e1d99 100644 --- a/src/vs/editor/test/common/viewModel/testViewModel.ts +++ b/src/vs/editor/test/common/viewModel/testViewModel.ts @@ -7,15 +7,15 @@ import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; 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'; export function testViewModel(text: string[], options: IEditorOptions, callback: (viewModel: ViewModel, model: TextModel) => void): void { const EDITOR_ID = 1; - let configuration = new TestConfiguration(options); - - let model = TextModel.createFromString(text.join('\n')); - - let viewModel = new ViewModel(EDITOR_ID, configuration, model, null!); + const configuration = new TestConfiguration(options); + const model = TextModel.createFromString(text.join('\n')); + const monospaceLineBreaksComputerFactory = MonospaceLineBreaksComputerFactory.create(configuration.options); + const viewModel = new ViewModel(EDITOR_ID, configuration, model, monospaceLineBreaksComputerFactory, monospaceLineBreaksComputerFactory, null!); callback(viewModel, model); diff --git a/src/vs/editor/test/node/classification/typescript-test.ts b/src/vs/editor/test/node/classification/typescript-test.ts index bf8856d2f7..f1a8a21c22 100644 --- a/src/vs/editor/test/node/classification/typescript-test.ts +++ b/src/vs/editor/test/node/classification/typescript-test.ts @@ -1,5 +1,5 @@ /// -/* tslint:disable */ +/* eslint-disable */ const x01 = "string"; /// ^^^^^^^^ string diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 5d3ab1653e..d4ddb3ccd1 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -405,6 +405,7 @@ declare namespace monaco { readonly leftButton: boolean; readonly middleButton: boolean; readonly rightButton: boolean; + readonly buttons: number; readonly target: HTMLElement; readonly detail: number; readonly posx: number; @@ -1048,10 +1049,58 @@ declare namespace monaco.editor { run(editor: ICodeEditor): void | Promise; } + /** + * Options which apply for all editors. + */ + export interface IGlobalEditorOptions { + /** + * The number of spaces a tab is equal to. + * This setting is overridden based on the file contents when `detectIndentation` is on. + * Defaults to 4. + */ + tabSize?: number; + /** + * Insert spaces when pressing `Tab`. + * This setting is overridden based on the file contents when detectIndentation` is on. + * Defaults to true. + */ + insertSpaces?: boolean; + /** + * Controls whether `tabSize` and `insertSpaces` will be automatically detected when a file is opened based on the file contents. + * Defaults to true. + */ + detectIndentation?: boolean; + /** + * Remove trailing auto inserted whitespace. + * Defaults to true. + */ + trimAutoWhitespace?: boolean; + /** + * Special handling for large files to disable certain memory intensive features. + * Defaults to true. + */ + largeFileOptimizations?: boolean; + /** + * Controls whether completions should be computed based on words in the document. + * Defaults to true. + */ + wordBasedSuggestions?: boolean; + /** + * Keep peek editors open even when double clicking their content or when hitting `Escape`. + * Defaults to false. + */ + stablePeek?: boolean; + /** + * Lines above this length will not be tokenized for performance reasons. + * Defaults to 20000. + */ + maxTokenizationLineLength?: number; + } + /** * The options to create an editor. */ - export interface IStandaloneEditorConstructionOptions extends IEditorConstructionOptions { + export interface IStandaloneEditorConstructionOptions extends IEditorConstructionOptions, IGlobalEditorOptions { /** * The initial model associated with this code editor. */ @@ -1096,6 +1145,7 @@ declare namespace monaco.editor { } export interface IStandaloneCodeEditor extends ICodeEditor { + updateOptions(newOptions: IEditorOptions & IGlobalEditorOptions): void; addCommand(keybinding: number, handler: ICommandHandler, context?: string): string | null; createContextKey(key: string, defaultValue: T): IContextKey; addAction(descriptor: IActionDescriptor): IDisposable; @@ -1857,14 +1907,14 @@ declare namespace monaco.editor { * @param range The range to replace (delete). May be empty to represent a simple insert. * @param text The text to replace with. May be null to represent a simple delete. */ - addEditOperation(range: Range, text: string | null): void; + addEditOperation(range: Range, text: string | null, forceMoveMarkers?: boolean): void; /** * Add a new edit operation (a replace operation). * The inverse edits will be accessible in `ICursorStateComputerData.getInverseEditOperations()` * @param range The range to replace (delete). May be empty to represent a simple insert. * @param text The text to replace with. May be null to represent a simple delete. */ - addTrackedEditOperation(range: Range, text: string | null): void; + addTrackedEditOperation(range: Range, text: string | null, forceMoveMarkers?: boolean): void; /** * Track `selection` when applying edit operations. * A best effort will be made to not grow/expand the selection. @@ -1972,6 +2022,13 @@ declare namespace monaco.editor { readonly charChanges: ICharChange[] | undefined; } + export interface IContentSizeChangedEvent { + readonly contentWidth: number; + readonly contentHeight: number; + readonly contentWidthChanged: boolean; + readonly contentHeightChanged: boolean; + } + export interface INewScrollPosition { scrollLeft?: number; scrollTop?: number; @@ -2424,6 +2481,15 @@ declare namespace monaco.editor { readonly reason: CursorChangeReason; } + export enum AccessibilitySupport { + /** + * This should be the browser case where it is not known if a screen reader is attached or no. + */ + Unknown = 0, + Disabled = 1, + Enabled = 2 + } + /** * Configuration options for auto closing quotes and brackets */ @@ -2439,6 +2505,17 @@ declare namespace monaco.editor { */ export type EditorAutoClosingOvertypeStrategy = 'always' | 'auto' | 'never'; + /** + * Configuration options for auto indentation in the editor + */ + export enum EditorAutoIndentStrategy { + None = 0, + Keep = 1, + Brackets = 2, + Advanced = 3, + Full = 4 + } + /** * Configuration options for the editor. */ @@ -2658,21 +2735,21 @@ declare namespace monaco.editor { * Defaults to 'same' in vscode and to 'none' in monaco-editor. */ wrappingIndent?: 'none' | 'same' | 'indent' | 'deepIndent'; + /** + * Controls the wrapping algorithm to use. + * Defaults to 'monospace'. + */ + wrappingAlgorithm?: 'monospace' | 'dom'; /** * Configure word wrapping characters. A break will be introduced before these characters. - * Defaults to '{([+'. + * Defaults to '([{‘“〈《「『【〔([{「£¥$£¥++'. */ wordWrapBreakBeforeCharacters?: string; /** * Configure word wrapping characters. A break will be introduced after these characters. - * Defaults to ' \t})]?|&,;'. + * Defaults to ' \t})]?|/&.,;¢°′″‰℃、。。、¢,.:;?!%・・ゝゞヽヾーァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ々〻ァィゥェォャュョッー”〉》」』】〕)]}」'. */ wordWrapBreakAfterCharacters?: string; - /** - * Configure word wrapping characters. A break will be introduced after these characters only if no `wordWrapBreakBeforeCharacters` or `wordWrapBreakAfterCharacters` were found. - * Defaults to '.'. - */ - wordWrapBreakObtrusiveCharacters?: string; /** * Performance guard: Stop rendering a line after x characters. * Defaults to 10000. @@ -2861,7 +2938,7 @@ declare namespace monaco.editor { */ codeActionsOnSaveTimeout?: number; /** - * Enable code folding + * Enable code folding. * Defaults to true. */ folding?: boolean; @@ -2870,6 +2947,11 @@ declare namespace monaco.editor { * Defaults to 'auto'. */ foldingStrategy?: 'auto' | 'indentation'; + /** + * Enable highlight for folded regions. + * Defaults to true. + */ + foldingHighlight?: boolean; /** * Controls whether the fold actions in the gutter stay always visible or hide unless the mouse is over the gutter. * Defaults to 'mouseover'. @@ -2933,6 +3015,11 @@ declare namespace monaco.editor { * Controls fading out of unused variables. */ showUnused?: boolean; + /** + * Controls whether to focus the inline editor in the peek widget by default. + * Defaults to false. + */ + peekWidgetFocusInlineEditor?: boolean; } export interface IEditorConstructionOptions extends IEditorOptions { @@ -2988,6 +3075,79 @@ declare namespace monaco.editor { export class ConfigurationChangedEvent { } + /** + * All computed editor options. + */ + export interface IComputedEditorOptions { + get(id: T): FindComputedEditorOptionValueById; + } + + export interface IEditorOption { + readonly id: K1; + readonly name: string; + defaultValue: V; + } + + /** + * The kind of animation in which the editor's cursor should be rendered. + */ + export enum TextEditorCursorBlinkingStyle { + /** + * Hidden + */ + Hidden = 0, + /** + * Blinking + */ + Blink = 1, + /** + * Blinking with smooth fading + */ + Smooth = 2, + /** + * Blinking with prolonged filled state and smooth fading + */ + Phase = 3, + /** + * Expand collapse animation on the y axis + */ + Expand = 4, + /** + * No-Blinking + */ + Solid = 5 + } + + /** + * The style in which the editor's cursor should be rendered. + */ + export enum TextEditorCursorStyle { + /** + * As a vertical line (sitting between two characters). + */ + Line = 1, + /** + * As a block (sitting on top of a character). + */ + Block = 2, + /** + * As a horizontal line (sitting under a character). + */ + Underline = 3, + /** + * As a thin vertical line (sitting between two characters). + */ + LineThin = 4, + /** + * As an outlined block (sitting on top of a character). + */ + BlockOutline = 5, + /** + * As a thin horizontal line (sitting under a character). + */ + UnderlineThin = 6 + } + /** * Configuration options for editor find widget */ @@ -3003,6 +3163,8 @@ declare namespace monaco.editor { addExtraSpaceOnTop?: boolean; } + export type EditorFindOptions = Readonly>; + export type GoToLocationValues = 'peek' | 'gotoAndPeek' | 'goto'; /** @@ -3022,6 +3184,8 @@ declare namespace monaco.editor { alternativeReferenceCommand?: string; } + export type GoToLocationOptions = Readonly>; + /** * Configuration options for editor hover */ @@ -3043,6 +3207,19 @@ declare namespace monaco.editor { sticky?: boolean; } + export type EditorHoverOptions = Readonly>; + + /** + * Configuration options for semantic highlighting + */ + export interface IEditorSemanticHighlightingOptions { + /** + * Enable semantic highlighting. + * Defaults to true. + */ + enabled?: boolean; + } + /** * A description for the overview ruler position. */ @@ -3091,10 +3268,6 @@ declare namespace monaco.editor { * The width of the glyph margin. */ readonly glyphMarginWidth: number; - /** - * The height of the glyph margin. - */ - readonly glyphMarginHeight: number; /** * Left position for the line numbers. */ @@ -3103,10 +3276,6 @@ declare namespace monaco.editor { * The width of the line numbers. */ readonly lineNumbersWidth: number; - /** - * The height of the line numbers. - */ - readonly lineNumbersHeight: number; /** * Left position for the line decorations. */ @@ -3115,10 +3284,6 @@ declare namespace monaco.editor { * The width of the line decorations. */ readonly decorationsWidth: number; - /** - * The height of the line decorations. - */ - readonly decorationsHeight: number; /** * Left position for the content (actual text) */ @@ -3127,10 +3292,6 @@ declare namespace monaco.editor { * The width of the content (actual text) */ readonly contentWidth: number; - /** - * The height of the content (actual height) - */ - readonly contentHeight: number; /** * The position for the minimap */ @@ -3172,6 +3333,8 @@ declare namespace monaco.editor { enabled?: boolean; } + export type EditorLightbulbOptions = Readonly>; + /** * Configuration options for editor minimap */ @@ -3207,6 +3370,8 @@ declare namespace monaco.editor { scale?: number; } + export type EditorMinimapOptions = Readonly>; + /** * Configuration options for parameter hints */ @@ -3223,6 +3388,8 @@ declare namespace monaco.editor { cycle?: boolean; } + export type InternalParameterHintOptions = Readonly>; + /** * Configuration options for quick suggestions */ @@ -3232,8 +3399,23 @@ declare namespace monaco.editor { strings: boolean; } + export type ValidQuickSuggestionsOptions = boolean | Readonly>; + export type LineNumbersType = 'on' | 'off' | 'relative' | 'interval' | ((lineNumber: number) => string); + export enum RenderLineNumbersType { + Off = 0, + On = 1, + Relative = 2, + Interval = 3, + Custom = 4 + } + + export interface InternalEditorRenderLineNumbersOptions { + readonly renderType: RenderLineNumbersType; + readonly renderFn: ((lineNumber: number) => string) | null; + } + /** * Configuration options for editor scrollbars */ @@ -3300,6 +3482,21 @@ declare namespace monaco.editor { horizontalSliderSize?: number; } + export interface InternalEditorScrollbarOptions { + readonly arrowSize: number; + readonly vertical: ScrollbarVisibility; + readonly horizontal: ScrollbarVisibility; + readonly useShadows: boolean; + readonly verticalHasArrows: boolean; + readonly horizontalHasArrows: boolean; + readonly handleMouseWheel: boolean; + readonly alwaysConsumeMouseWheel: boolean; + readonly horizontalScrollbarSize: number; + readonly horizontalSliderSize: number; + readonly verticalScrollbarSize: number; + readonly verticalSliderSize: number; + } + /** * Configuration options for editor suggest widget */ @@ -3438,6 +3635,268 @@ declare namespace monaco.editor { showSnippets?: boolean; } + export type InternalSuggestOptions = Readonly>; + + /** + * Describes how to indent wrapped lines. + */ + export enum WrappingIndent { + /** + * No indentation => wrapped lines begin at column 1. + */ + None = 0, + /** + * Same => wrapped lines get the same indentation as the parent. + */ + Same = 1, + /** + * Indent => wrapped lines get +1 indentation toward the parent. + */ + Indent = 2, + /** + * DeepIndent => wrapped lines get +2 indentation toward the parent. + */ + DeepIndent = 3 + } + + export interface EditorWrappingInfo { + readonly isDominatedByLongLines: boolean; + readonly isWordWrapMinified: boolean; + readonly isViewportWrapping: boolean; + readonly wrappingColumn: number; + } + + export enum EditorOption { + acceptSuggestionOnCommitCharacter = 0, + acceptSuggestionOnEnter = 1, + accessibilitySupport = 2, + accessibilityPageSize = 3, + ariaLabel = 4, + autoClosingBrackets = 5, + autoClosingOvertype = 6, + autoClosingQuotes = 7, + autoIndent = 8, + automaticLayout = 9, + autoSurround = 10, + codeLens = 11, + colorDecorators = 12, + contextmenu = 13, + copyWithSyntaxHighlighting = 14, + cursorBlinking = 15, + cursorSmoothCaretAnimation = 16, + cursorStyle = 17, + cursorSurroundingLines = 18, + cursorSurroundingLinesStyle = 19, + cursorWidth = 20, + disableLayerHinting = 21, + disableMonospaceOptimizations = 22, + dragAndDrop = 23, + emptySelectionClipboard = 24, + extraEditorClassName = 25, + fastScrollSensitivity = 26, + find = 27, + fixedOverflowWidgets = 28, + folding = 29, + foldingStrategy = 30, + foldingHighlight = 31, + fontFamily = 32, + fontInfo = 33, + fontLigatures = 34, + fontSize = 35, + fontWeight = 36, + formatOnPaste = 37, + formatOnType = 38, + glyphMargin = 39, + gotoLocation = 40, + hideCursorInOverviewRuler = 41, + highlightActiveIndentGuide = 42, + hover = 43, + inDiffEditor = 44, + letterSpacing = 45, + lightbulb = 46, + lineDecorationsWidth = 47, + lineHeight = 48, + lineNumbers = 49, + lineNumbersMinChars = 50, + links = 51, + matchBrackets = 52, + minimap = 53, + mouseStyle = 54, + mouseWheelScrollSensitivity = 55, + mouseWheelZoom = 56, + multiCursorMergeOverlapping = 57, + multiCursorModifier = 58, + multiCursorPaste = 59, + occurrencesHighlight = 60, + overviewRulerBorder = 61, + overviewRulerLanes = 62, + parameterHints = 63, + peekWidgetFocusInlineEditor = 64, + quickSuggestions = 65, + quickSuggestionsDelay = 66, + readOnly = 67, + renderControlCharacters = 68, + renderIndentGuides = 69, + renderFinalNewline = 70, + renderLineHighlight = 71, + renderWhitespace = 72, + revealHorizontalRightPadding = 73, + roundedSelection = 74, + rulers = 75, + scrollbar = 76, + scrollBeyondLastColumn = 77, + scrollBeyondLastLine = 78, + selectionClipboard = 79, + selectionHighlight = 80, + selectOnLineNumbers = 81, + semanticHighlighting = 82, + showFoldingControls = 83, + showUnused = 84, + snippetSuggestions = 85, + smoothScrolling = 86, + stopRenderingLineAfter = 87, + suggest = 88, + suggestFontSize = 89, + suggestLineHeight = 90, + suggestOnTriggerCharacters = 91, + suggestSelection = 92, + tabCompletion = 93, + useTabStops = 94, + wordSeparators = 95, + wordWrap = 96, + wordWrapBreakAfterCharacters = 97, + wordWrapBreakBeforeCharacters = 98, + wordWrapColumn = 99, + wordWrapMinified = 100, + wrappingIndent = 101, + wrappingAlgorithm = 102, + editorClassName = 103, + pixelRatio = 104, + tabFocusMode = 105, + layoutInfo = 106, + wrappingInfo = 107 + } + export const EditorOptions: { + acceptSuggestionOnCommitCharacter: IEditorOption; + acceptSuggestionOnEnter: IEditorOption; + accessibilitySupport: IEditorOption; + accessibilityPageSize: IEditorOption; + ariaLabel: IEditorOption; + autoClosingBrackets: IEditorOption; + autoClosingOvertype: IEditorOption; + autoClosingQuotes: IEditorOption; + autoIndent: IEditorOption; + automaticLayout: IEditorOption; + autoSurround: IEditorOption; + codeLens: IEditorOption; + colorDecorators: IEditorOption; + contextmenu: IEditorOption; + copyWithSyntaxHighlighting: IEditorOption; + cursorBlinking: IEditorOption; + cursorSmoothCaretAnimation: IEditorOption; + cursorStyle: IEditorOption; + cursorSurroundingLines: IEditorOption; + cursorSurroundingLinesStyle: IEditorOption; + cursorWidth: IEditorOption; + disableLayerHinting: IEditorOption; + disableMonospaceOptimizations: IEditorOption; + dragAndDrop: IEditorOption; + emptySelectionClipboard: IEditorOption; + extraEditorClassName: IEditorOption; + fastScrollSensitivity: IEditorOption; + find: IEditorOption; + fixedOverflowWidgets: IEditorOption; + folding: IEditorOption; + foldingStrategy: IEditorOption; + foldingHighlight: IEditorOption; + fontFamily: IEditorOption; + fontInfo: IEditorOption; + fontLigatures2: IEditorOption; + fontSize: IEditorOption; + fontWeight: IEditorOption; + formatOnPaste: IEditorOption; + formatOnType: IEditorOption; + glyphMargin: IEditorOption; + gotoLocation: IEditorOption; + hideCursorInOverviewRuler: IEditorOption; + highlightActiveIndentGuide: IEditorOption; + hover: IEditorOption; + inDiffEditor: IEditorOption; + letterSpacing: IEditorOption; + lightbulb: IEditorOption; + lineDecorationsWidth: IEditorOption; + lineHeight: IEditorOption; + lineNumbers: IEditorOption; + lineNumbersMinChars: IEditorOption; + links: IEditorOption; + matchBrackets: IEditorOption; + minimap: IEditorOption; + mouseStyle: IEditorOption; + mouseWheelScrollSensitivity: IEditorOption; + mouseWheelZoom: IEditorOption; + multiCursorMergeOverlapping: IEditorOption; + multiCursorModifier: IEditorOption; + multiCursorPaste: IEditorOption; + occurrencesHighlight: IEditorOption; + overviewRulerBorder: IEditorOption; + overviewRulerLanes: IEditorOption; + parameterHints: IEditorOption; + peekWidgetFocusInlineEditor: IEditorOption; + quickSuggestions: IEditorOption; + quickSuggestionsDelay: IEditorOption; + readOnly: IEditorOption; + renderControlCharacters: IEditorOption; + renderIndentGuides: IEditorOption; + renderFinalNewline: IEditorOption; + renderLineHighlight: IEditorOption; + renderWhitespace: IEditorOption; + revealHorizontalRightPadding: IEditorOption; + roundedSelection: IEditorOption; + rulers: IEditorOption; + scrollbar: IEditorOption; + scrollBeyondLastColumn: IEditorOption; + scrollBeyondLastLine: IEditorOption; + selectionClipboard: IEditorOption; + selectionHighlight: IEditorOption; + selectOnLineNumbers: IEditorOption; + semanticHighlighting: IEditorOption; + showFoldingControls: IEditorOption; + showUnused: IEditorOption; + snippetSuggestions: IEditorOption; + smoothScrolling: IEditorOption; + stopRenderingLineAfter: IEditorOption; + suggest: IEditorOption; + suggestFontSize: IEditorOption; + suggestLineHeight: IEditorOption; + suggestOnTriggerCharacters: IEditorOption; + suggestSelection: IEditorOption; + tabCompletion: IEditorOption; + useTabStops: IEditorOption; + wordSeparators: IEditorOption; + wordWrap: IEditorOption; + wordWrapBreakAfterCharacters: IEditorOption; + wordWrapBreakBeforeCharacters: IEditorOption; + wordWrapColumn: IEditorOption; + wordWrapMinified: IEditorOption; + wrappingIndent: IEditorOption; + wrappingAlgorithm: IEditorOption; + editorClassName: IEditorOption; + pixelRatio: IEditorOption; + tabFocusMode: IEditorOption; + layoutInfo: IEditorOption; + wrappingInfo: IEditorOption; + }; + + type EditorOptionsType = typeof EditorOptions; + + type FindEditorOptionsKeyById = { + [K in keyof EditorOptionsType]: EditorOptionsType[K]['id'] extends T ? K : never; + }[keyof EditorOptionsType]; + + type ComputedEditorOptionValue> = T extends IEditorOption ? R : never; + + export type FindComputedEditorOptionValueById = NonNullable]>>; + /** * A view zone is a full horizontal rectangle that 'pushes' text down. * The editor reserves space for view zones when rendering. @@ -3852,6 +4311,11 @@ declare namespace monaco.editor { * @event */ onDidLayoutChange(listener: (e: EditorLayoutInfo) => void): IDisposable; + /** + * An event emitted when the content width or content height in the editor has changed. + * @event + */ + onDidContentSizeChange(listener: (e: IContentSizeChangedEvent) => void): IDisposable; /** * An event emitted when the scroll in the editor has changed. * @event @@ -3888,6 +4352,14 @@ declare namespace monaco.editor { * It is safe to call setModel(null) to simply detach the current model from the editor. */ setModel(model: ITextModel | null): void; + /** + * Gets all the editor computed options. + */ + getOptions(): IComputedEditorOptions; + /** + * Gets a specific editor option. + */ + getOption(id: T): FindComputedEditorOptionValueById; /** * Returns the editor's configuration (without any validation or defaults). */ @@ -3905,6 +4377,11 @@ declare namespace monaco.editor { * @see `ITextModel.setValue` */ setValue(newValue: string): void; + /** + * Get the width of the editor's content. + * This is information that is "erased" when computing `scrollWidth = Math.max(contentWidth, width)` + */ + getContentWidth(): number; /** * Get the scrollWidth of the editor's viewport. */ @@ -3913,6 +4390,11 @@ declare namespace monaco.editor { * Get the scrollLeft of the editor's viewport. */ getScrollLeft(): number; + /** + * Get the height of the editor's content. + * This is information that is "erased" when computing `scrollHeight = Math.max(contentHeight, height)` + */ + getContentHeight(): number; /** * Get the scrollHeight of the editor's viewport. */ @@ -4804,6 +5286,7 @@ declare namespace monaco.languages { export interface CompletionList { suggestions: CompletionItem[]; incomplete?: boolean; + isDetailsResolved?: boolean; dispose?(): void; } @@ -5395,7 +5878,7 @@ declare namespace monaco.languages { constructor(value: string); } - export interface ResourceFileEdit { + export interface WorkspaceFileEdit { oldUri?: Uri; newUri?: Uri; options?: { @@ -5406,14 +5889,14 @@ declare namespace monaco.languages { }; } - export interface ResourceTextEdit { + export interface WorkspaceTextEdit { resource: Uri; modelVersionId?: number; edits: TextEdit[]; } export interface WorkspaceEdit { - edits: Array; + edits: Array; } export interface Rejection { @@ -5475,10 +5958,15 @@ declare namespace monaco.languages { readonly edits: SemanticTokensEdit[]; } - export interface SemanticTokensProvider { + export interface DocumentSemanticTokensProvider { getLegend(): SemanticTokensLegend; - provideSemanticTokens(model: editor.ITextModel, lastResultId: string | null, ranges: Range[] | null, token: CancellationToken): ProviderResult; - releaseSemanticTokens(resultId: string | undefined): void; + provideDocumentSemanticTokens(model: editor.ITextModel, lastResultId: string | null, token: CancellationToken): ProviderResult; + releaseDocumentSemanticTokens(resultId: string | undefined): void; + } + + export interface DocumentRangeSemanticTokensProvider { + getLegend(): SemanticTokensLegend; + provideDocumentRangeSemanticTokens(model: editor.ITextModel, range: Range, token: CancellationToken): ProviderResult; } export interface ILanguageExtensionPoint { @@ -5639,4 +6127,4 @@ declare namespace monaco.worker { } -//dtsv=2 +//dtsv=3 diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index af1fcb1710..efb5b01268 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -5,11 +5,11 @@ import { Action } from 'vs/base/common/actions'; import { SyncDescriptor0, createSyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { IConstructorSignature2, createDecorator, BrandedService } from 'vs/platform/instantiation/common/instantiation'; -import { IKeybindings, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { IConstructorSignature2, createDecorator, BrandedService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindings, KeybindingsRegistry, IKeybindingRule } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { ICommandService, ICommandHandler, CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; @@ -70,6 +70,8 @@ export const enum MenuId { EditorTitleContext, EmptyEditorGroupContext, ExplorerContext, + ExtensionContext, + GlobalActivity, MenubarAppearanceMenu, MenubarDebugMenu, MenubarEditMenu, @@ -111,7 +113,8 @@ export const enum MenuId { CommentThreadActions, CommentTitle, CommentActions, - GlobalActivity + BulkEditTitle, + BulkEditContext, } export interface IMenuActionOptions { @@ -347,62 +350,50 @@ export class SyncActionDescriptor { } } +//#region --- IAction2 -export interface IActionDescriptor { - id: string; - handler: ICommandHandler; +type OneOrN = T | T[]; - // ICommandUI - title?: ILocalizedString; - category?: string; +export interface IAction2Options extends ICommandAction { f1?: boolean; - - // - menu?: { - menuId: MenuId, - when?: ContextKeyExpr; - group?: string; - }; - - // - keybinding?: { - when?: ContextKeyExpr; - weight?: number; - keys: IKeybindings; - }; + menu?: OneOrN<{ id: MenuId } & Omit>; + keybinding?: Omit; } +export abstract class Action2 { + constructor(readonly desc: Readonly) { } + abstract run(accessor: ServicesAccessor, ...args: any[]): any; +} -export function registerAction(desc: IActionDescriptor) { +export function registerAction2(ctor: { new(): Action2 }): IDisposable { + const disposables = new DisposableStore(); + const action = new ctor(); + disposables.add(CommandsRegistry.registerCommand({ + id: action.desc.id, + handler: (accessor, ...args) => action.run(accessor, ...args), + description: undefined, + })); - const { id, handler, title, category, menu, keybinding } = desc; - - // 1) register as command - CommandsRegistry.registerCommand(id, handler); - - // 2) menus - if (menu && title) { - let command = { id, title, category }; - let { menuId, when, group } = menu; - MenuRegistry.appendMenuItem(menuId, { - command, - when, - group - }); + if (Array.isArray(action.desc.menu)) { + for (let item of action.desc.menu) { + disposables.add(MenuRegistry.appendMenuItem(item.id, { command: action.desc, ...item })); + } + } else if (action.desc.menu) { + disposables.add(MenuRegistry.appendMenuItem(action.desc.menu.id, { command: action.desc, ...action.desc.menu })); } - // 3) keybindings - if (keybinding) { - let { when, weight, keys } = keybinding; + if (action.desc.f1) { + disposables.add(MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: action.desc, ...action.desc })); + } + + if (action.desc.keybinding) { KeybindingsRegistry.registerKeybindingRule({ - id, - when, - weight: weight || 0, - primary: keys.primary, - secondary: keys.secondary, - linux: keys.linux, - mac: keys.mac, - win: keys.win + ...action.desc.keybinding, + id: action.desc.id, + when: ContextKeyExpr.and(action.desc.precondition, action.desc.keybinding.when) }); } + + return disposables; } +//#endregion diff --git a/src/vs/platform/auth/common/auth.ts b/src/vs/platform/auth/common/auth.ts deleted file mode 100644 index 7ca970155d..0000000000 --- a/src/vs/platform/auth/common/auth.ts +++ /dev/null @@ -1,31 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { Event, Emitter } from 'vs/base/common/event'; -import { URI } from 'vs/base/common/uri'; - -export const enum AuthTokenStatus { - Initializing = 'Initializing', - SignedOut = 'SignedOut', - SignedIn = 'SignedIn', - SigningIn = 'SigningIn', - RefreshingToken = 'RefreshingToken' -} - -export const IAuthTokenService = createDecorator('IAuthTokenService'); - -export interface IAuthTokenService { - _serviceBrand: undefined; - - readonly status: AuthTokenStatus; - readonly onDidChangeStatus: Event; - readonly _onDidGetCallback: Emitter; - - getToken(): Promise; - refreshToken(): Promise; - login(): Promise; - logout(): Promise; -} diff --git a/src/vs/platform/auth/common/authTokenIpc.ts b/src/vs/platform/auth/common/authTokenIpc.ts deleted file mode 100644 index 7e6323b192..0000000000 --- a/src/vs/platform/auth/common/authTokenIpc.ts +++ /dev/null @@ -1,31 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { Event } from 'vs/base/common/event'; -import { IAuthTokenService } from 'vs/platform/auth/common/auth'; - -export class AuthTokenChannel implements IServerChannel { - - constructor(private readonly service: IAuthTokenService) { } - - listen(_: unknown, event: string): Event { - switch (event) { - case 'onDidChangeStatus': return this.service.onDidChangeStatus; - } - throw new Error(`Event not found: ${event}`); - } - - call(context: any, command: string, args?: any): Promise { - switch (command) { - case '_getInitialStatus': return Promise.resolve(this.service.status); - case 'getToken': return this.service.getToken(); - case 'refreshToken': return this.service.refreshToken(); - case 'login': return this.service.login(); - case 'logout': return this.service.logout(); - } - throw new Error('Invalid call'); - } -} diff --git a/src/vs/platform/auth/electron-browser/authTokenService.ts b/src/vs/platform/auth/electron-browser/authTokenService.ts deleted file mode 100644 index e299ea0f4f..0000000000 --- a/src/vs/platform/auth/electron-browser/authTokenService.ts +++ /dev/null @@ -1,276 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as crypto from 'crypto'; -import * as https from 'https'; -import { Event, Emitter } from 'vs/base/common/event'; -import { IAuthTokenService, AuthTokenStatus } from 'vs/platform/auth/common/auth'; -import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; -import { generateUuid } from 'vs/base/common/uuid'; -import { shell } from 'electron'; -import { createServer, startServer } from 'vs/platform/auth/electron-browser/authServer'; -import { IProductService } from 'vs/platform/product/common/productService'; - -const SERVICE_NAME = 'VS Code'; -const ACCOUNT = 'MyAccount'; - -const activeDirectoryResourceId = 'https://management.core.windows.net/'; - -function toQuery(obj: any): string { - return Object.keys(obj).map(key => `${key}=${obj[key]}`).join('&'); -} - -function toBase64UrlEncoding(base64string: string) { - return base64string.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); // Need to use base64url encoding -} - -export interface IToken { - expiresIn: string; // How long access token is valid, in seconds - expiresOn: string; // When the access token expires in epoch time - accessToken: string; - refreshToken: string; -} - -export class AuthTokenService extends Disposable implements IAuthTokenService { - _serviceBrand: undefined; - - private _status: AuthTokenStatus = AuthTokenStatus.Initializing; - get status(): AuthTokenStatus { return this._status; } - private _onDidChangeStatus: Emitter = this._register(new Emitter()); - readonly onDidChangeStatus: Event = this._onDidChangeStatus.event; - - public readonly _onDidGetCallback: Emitter = this._register(new Emitter()); - readonly onDidGetCallback: Event = this._onDidGetCallback.event; - - private _activeToken: IToken | undefined; - - constructor( - @ICredentialsService private readonly credentialsService: ICredentialsService, - @IProductService private readonly productService: IProductService - ) { - super(); - if (!this.productService.auth) { - return; - } - - this.credentialsService.getPassword(SERVICE_NAME, ACCOUNT).then(storedRefreshToken => { - if (storedRefreshToken) { - this.refresh(storedRefreshToken); - } else { - this.setStatus(AuthTokenStatus.SignedOut); - } - }); - } - - public async login(): Promise { - if (!this.productService.auth) { - throw new Error('Authentication is not configured.'); - } - - this.setStatus(AuthTokenStatus.SigningIn); - - const nonce = generateUuid(); - const { server, redirectPromise, codePromise } = createServer(nonce); - - try { - const port = await startServer(server); - shell.openExternal(`http://localhost:${port}/signin?nonce=${encodeURIComponent(nonce)}`); - - const redirectReq = await redirectPromise; - if ('err' in redirectReq) { - const { err, res } = redirectReq; - res.writeHead(302, { Location: `/?error=${encodeURIComponent(err && err.message || 'Unkown error')}` }); - res.end(); - throw err; - } - - const host = redirectReq.req.headers.host || ''; - const updatedPortStr = (/^[^:]+:(\d+)$/.exec(Array.isArray(host) ? host[0] : host) || [])[1]; - const updatedPort = updatedPortStr ? parseInt(updatedPortStr, 10) : port; - - const state = `${updatedPort},${encodeURIComponent(nonce)}`; - - const codeVerifier = toBase64UrlEncoding(crypto.randomBytes(32).toString('base64')); - const codeChallenge = toBase64UrlEncoding(crypto.createHash('sha256').update(codeVerifier).digest('base64')); - - let uri = URI.parse(this.productService.auth.loginUrl); - uri = uri.with({ - query: `response_type=code&client_id=${encodeURIComponent(this.productService.auth.clientId)}&redirect_uri=${this.productService.auth.redirectUrl}&state=${encodeURIComponent(state)}&resource=${activeDirectoryResourceId}&prompt=select_account&code_challenge_method=S256&code_challenge=${codeChallenge}` - }); - - await redirectReq.res.writeHead(302, { Location: uri.toString(true) }); - redirectReq.res.end(); - - const codeRes = await codePromise; - const res = codeRes.res; - - try { - if ('err' in codeRes) { - throw codeRes.err; - } - const token = await this.exchangeCodeForToken(codeRes.code, codeVerifier); - this.setToken(token); - res.writeHead(302, { Location: '/' }); - res.end(); - } catch (err) { - res.writeHead(302, { Location: `/?error=${encodeURIComponent(err && err.message || 'Unkown error')}` }); - res.end(); - } - } finally { - setTimeout(() => { - server.close(); - }, 5000); - } - - } - - public getToken(): Promise { - return Promise.resolve(this._activeToken?.accessToken); - } - - public async refreshToken(): Promise { - if (!this._activeToken) { - throw new Error('No token to refresh'); - } - - this.refresh(this._activeToken.refreshToken); - } - - private setToken(token: IToken) { - this._activeToken = token; - this.credentialsService.setPassword(SERVICE_NAME, ACCOUNT, token.refreshToken); - this.setStatus(AuthTokenStatus.SignedIn); - } - - private exchangeCodeForToken(code: string, codeVerifier: string): Promise { - return new Promise((resolve: (value: IToken) => void, reject) => { - try { - if (!this.productService.auth) { - throw new Error('Authentication is not configured.'); - } - - const postData = toQuery({ - grant_type: 'authorization_code', - code: code, - client_id: this.productService.auth?.clientId, - code_verifier: codeVerifier, - redirect_uri: this.productService.auth?.redirectUrl - }); - - const tokenUrl = URI.parse(this.productService.auth.tokenUrl); - - const post = https.request({ - host: tokenUrl.authority, - path: tokenUrl.path, - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': postData.length - } - }, result => { - const buffer: Buffer[] = []; - result.on('data', (chunk: Buffer) => { - buffer.push(chunk); - }); - result.on('end', () => { - if (result.statusCode === 200) { - const json = JSON.parse(Buffer.concat(buffer).toString()); - resolve({ - expiresIn: json.access_token, - expiresOn: json.expires_on, - accessToken: json.access_token, - refreshToken: json.refresh_token - }); - } else { - reject(new Error('Bad!')); - } - }); - }); - - post.write(postData); - - post.end(); - post.on('error', err => { - reject(err); - }); - - } catch (e) { - reject(e); - } - }); - } - - private async refresh(refreshToken: string): Promise { - return new Promise((resolve, reject) => { - if (!this.productService.auth) { - throw new Error('Authentication is not configured.'); - } - - this.setStatus(AuthTokenStatus.RefreshingToken); - const postData = toQuery({ - refresh_token: refreshToken, - client_id: this.productService.auth?.clientId, - grant_type: 'refresh_token', - resource: activeDirectoryResourceId - }); - - const tokenUrl = URI.parse(this.productService.auth.tokenUrl); - - const post = https.request({ - host: tokenUrl.authority, - path: tokenUrl.path, - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': postData.length - } - }, result => { - const buffer: Buffer[] = []; - result.on('data', (chunk: Buffer) => { - buffer.push(chunk); - }); - result.on('end', () => { - if (result.statusCode === 200) { - const json = JSON.parse(Buffer.concat(buffer).toString()); - this.setToken({ - expiresIn: json.access_token, - expiresOn: json.expires_on, - accessToken: json.access_token, - refreshToken: json.refresh_token - }); - resolve(); - } else { - reject(new Error('Refreshing token failed.')); - } - }); - }); - - post.write(postData); - - post.end(); - post.on('error', err => { - this.setStatus(AuthTokenStatus.SignedOut); - reject(err); - }); - }); - } - - async logout(): Promise { - await this.credentialsService.deletePassword(SERVICE_NAME, ACCOUNT); - this._activeToken = undefined; - this.setStatus(AuthTokenStatus.SignedOut); - } - - private setStatus(status: AuthTokenStatus): void { - if (this._status !== status) { - this._status = status; - this._onDidChangeStatus.fire(status); - } - } - -} - diff --git a/src/vs/platform/configuration/common/configuration.ts b/src/vs/platform/configuration/common/configuration.ts index 298389a7b9..f2ad5ca748 100644 --- a/src/vs/platform/configuration/common/configuration.ts +++ b/src/vs/platform/configuration/common/configuration.ts @@ -83,6 +83,8 @@ export interface IConfigurationValue { readonly workspace?: { value?: T, override?: T }; readonly workspaceFolder?: { value?: T, override?: T }; readonly memory?: { value?: T, override?: T }; + + readonly overrideIdentifiers?: string[]; } export interface IConfigurationService { diff --git a/src/vs/platform/configuration/common/configurationModels.ts b/src/vs/platform/configuration/common/configurationModels.ts index 427493a49e..1ad58afc9d 100644 --- a/src/vs/platform/configuration/common/configurationModels.ts +++ b/src/vs/platform/configuration/common/configurationModels.ts @@ -392,6 +392,7 @@ export class Configuration { const workspaceFolderValue = folderConfigurationModel ? overrides.overrideIdentifier ? folderConfigurationModel.freeze().override(overrides.overrideIdentifier).getValue(key) : folderConfigurationModel.freeze().getValue(key) : undefined; const memoryValue = overrides.overrideIdentifier ? memoryConfigurationModel.override(overrides.overrideIdentifier).getValue(key) : memoryConfigurationModel.getValue(key); const value = consolidateConfigurationModel.getValue(key); + const overrideIdentifiers: string[] = arrays.distinct(arrays.flatten(consolidateConfigurationModel.overrides.map(override => override.identifiers))).filter(overrideIdentifier => consolidateConfigurationModel.getOverrideValue(key, overrideIdentifier) !== undefined); return { defaultValue: defaultValue, @@ -410,6 +411,8 @@ export class Configuration { workspace: workspaceValue !== undefined ? { value: this._workspaceConfiguration.freeze().getValue(key), override: overrides.overrideIdentifier ? this._workspaceConfiguration.freeze().getOverrideValue(key, overrides.overrideIdentifier) : undefined } : undefined, workspaceFolder: workspaceFolderValue !== undefined ? { value: folderConfigurationModel?.freeze().getValue(key), override: overrides.overrideIdentifier ? folderConfigurationModel?.freeze().getOverrideValue(key, overrides.overrideIdentifier) : undefined } : undefined, memory: memoryValue !== undefined ? { value: memoryConfigurationModel.getValue(key), override: overrides.overrideIdentifier ? memoryConfigurationModel.getOverrideValue(key, overrides.overrideIdentifier) : undefined } : undefined, + + overrideIdentifiers: overrideIdentifiers.length ? overrideIdentifiers : undefined }; } diff --git a/src/vs/platform/configuration/common/configurationRegistry.ts b/src/vs/platform/configuration/common/configurationRegistry.ts index 37d86209d4..46e309a1e7 100644 --- a/src/vs/platform/configuration/common/configurationRegistry.ts +++ b/src/vs/platform/configuration/common/configurationRegistry.ts @@ -8,9 +8,9 @@ import { Event, Emitter } from 'vs/base/common/event'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { Registry } from 'vs/platform/registry/common/platform'; import * as types from 'vs/base/common/types'; -import * as strings from 'vs/base/common/strings'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { values } from 'vs/base/common/map'; export const Extensions = { Configuration: 'base.contributions.configuration' @@ -157,8 +157,7 @@ class ConfigurationRegistry implements IConfigurationRegistry { private readonly configurationProperties: { [qualifiedKey: string]: IJSONSchema }; private readonly excludedConfigurationProperties: { [qualifiedKey: string]: IJSONSchema }; private readonly resourceLanguageSettingsSchema: IJSONSchema; - private readonly overrideIdentifiers: string[] = []; - private overridePropertyPattern: string; + private readonly overrideIdentifiers = new Set(); private readonly _onDidSchemaChange = new Emitter(); readonly onDidSchemaChange: Event = this._onDidSchemaChange.event; @@ -176,7 +175,6 @@ class ConfigurationRegistry implements IConfigurationRegistry { this.resourceLanguageSettingsSchema = { properties: {}, patternProperties: {}, additionalProperties: false, errorMessage: 'Unknown editor configuration setting', allowTrailingCommas: true, allowComments: true }; this.configurationProperties = {}; this.excludedConfigurationProperties = {}; - this.overridePropertyPattern = this.computeOverridePropertyPattern(); contributionRegistry.registerSchema(resourceLanguageSettingsSchemaId, this.resourceLanguageSettingsSchema); } @@ -290,7 +288,10 @@ class ConfigurationRegistry implements IConfigurationRegistry { } public registerOverrideIdentifiers(overrideIdentifiers: string[]): void { - this.overrideIdentifiers.push(...overrideIdentifiers); + for (const overrideIdentifier of overrideIdentifiers) { + this.overrideIdentifiers.add(overrideIdentifier); + } + this.updateOverridePropertyPatternKey(); } @@ -300,9 +301,7 @@ class ConfigurationRegistry implements IConfigurationRegistry { let properties = configuration.properties; if (properties) { for (let key in properties) { - let message; - if (validate && (message = validateProperty(key))) { - console.warn(message); + if (validate && validateProperty(key)) { delete properties[key]; continue; } @@ -390,42 +389,27 @@ class ConfigurationRegistry implements IConfigurationRegistry { } private updateOverridePropertyPatternKey(): void { - let patternProperties: IJSONSchema = allSettings.patternProperties[this.overridePropertyPattern]; - if (!patternProperties) { - patternProperties = { + for (const overrideIdentifier of values(this.overrideIdentifiers)) { + const overrideIdentifierProperty = `[${overrideIdentifier}]`; + const resourceLanguagePropertiesSchema: IJSONSchema = { type: 'object', description: nls.localize('overrideSettings.defaultDescription', "Configure editor settings to be overridden for a language."), errorMessage: 'Unknown Identifier. Use language identifiers', - $ref: resourceLanguageSettingsSchemaId + $ref: resourceLanguageSettingsSchemaId, + default: this.defaultOverridesConfigurationNode.properties![overrideIdentifierProperty]?.default }; + allSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema; + applicationSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema; + machineSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema; + machineOverridableSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema; + windowSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema; + resourceSettings.properties[overrideIdentifierProperty] = resourceLanguagePropertiesSchema; } - - delete allSettings.patternProperties[this.overridePropertyPattern]; - delete applicationSettings.patternProperties[this.overridePropertyPattern]; - delete machineSettings.patternProperties[this.overridePropertyPattern]; - delete machineOverridableSettings.patternProperties[this.overridePropertyPattern]; - delete windowSettings.patternProperties[this.overridePropertyPattern]; - delete resourceSettings.patternProperties[this.overridePropertyPattern]; - - this.overridePropertyPattern = this.computeOverridePropertyPattern(); - - allSettings.patternProperties[this.overridePropertyPattern] = patternProperties; - applicationSettings.patternProperties[this.overridePropertyPattern] = patternProperties; - machineSettings.patternProperties[this.overridePropertyPattern] = patternProperties; - machineOverridableSettings.patternProperties[this.overridePropertyPattern] = patternProperties; - windowSettings.patternProperties[this.overridePropertyPattern] = patternProperties; - resourceSettings.patternProperties[this.overridePropertyPattern] = patternProperties; - this._onDidSchemaChange.fire(); } - - private computeOverridePropertyPattern(): string { - return this.overrideIdentifiers.length ? OVERRIDE_PATTERN_WITH_SUBSTITUTION.replace('${0}', this.overrideIdentifiers.map(identifier => strings.createRegExp(identifier, false).source).join('|')) : OVERRIDE_PROPERTY; - } } const OVERRIDE_PROPERTY = '\\[.*\\]$'; -const OVERRIDE_PATTERN_WITH_SUBSTITUTION = '\\[(${0})\\]$'; export const OVERRIDE_PROPERTY_PATTERN = new RegExp(OVERRIDE_PROPERTY); export function getDefaultValue(type: string | string[] | undefined): any { diff --git a/src/vs/platform/configuration/test/common/configurationModels.test.ts b/src/vs/platform/configuration/test/common/configurationModels.test.ts index f139c25102..61a22b05a9 100644 --- a/src/vs/platform/configuration/test/common/configurationModels.test.ts +++ b/src/vs/platform/configuration/test/common/configurationModels.test.ts @@ -361,6 +361,17 @@ suite('CustomConfigurationModel', () => { suite('Configuration', () => { + test('Test inspect for overrideIdentifiers', () => { + const defaultConfigurationModel = parseConfigurationModel({ '[l1]': { 'a': 1 }, '[l2]': { 'b': 1 } }); + const userConfigurationModel = parseConfigurationModel({ '[l3]': { 'a': 2 } }); + const workspaceConfigurationModel = parseConfigurationModel({ '[l1]': { 'a': 3 }, '[l4]': { 'a': 3 } }); + const testObject: Configuration = new Configuration(defaultConfigurationModel, userConfigurationModel, new ConfigurationModel(), workspaceConfigurationModel); + + const { overrideIdentifiers } = testObject.inspect('a', {}, undefined); + + assert.deepEqual(overrideIdentifiers, ['l1', 'l3', 'l4']); + }); + test('Test update value', () => { const parser = new ConfigurationModelParser('test'); parser.parseContent(JSON.stringify({ 'a': 1 })); @@ -468,7 +479,7 @@ suite('Configuration', () => { }); - test('Test compare and deletre workspace folder configuration', () => { + test('Test compare and delete workspace folder configuration', () => { const testObject = new Configuration(new ConfigurationModel(), new ConfigurationModel()); testObject.updateFolderConfiguration(URI.file('file1'), toConfigurationModel({ 'editor.lineNumbers': 'off', @@ -484,6 +495,12 @@ suite('Configuration', () => { }); + function parseConfigurationModel(content: any): ConfigurationModel { + const parser = new ConfigurationModelParser('test'); + parser.parseContent(JSON.stringify(content)); + return parser.configurationModel; + } + }); suite('ConfigurationChangeEvent', () => { diff --git a/src/vs/platform/contextkey/common/contextkey.ts b/src/vs/platform/contextkey/common/contextkey.ts index df7ad84d1d..83a74bd8a6 100644 --- a/src/vs/platform/contextkey/common/contextkey.ts +++ b/src/vs/platform/contextkey/common/contextkey.ts @@ -309,10 +309,9 @@ export class ContextKeyEqualsExpr implements ContextKeyExpr { } public evaluate(context: IContext): boolean { - /* tslint:disable:triple-equals */ // Intentional == + // eslint-disable-next-line eqeqeq return (context.getValue(this.key) == this.value); - /* tslint:enable:triple-equals */ } public serialize(): string { @@ -375,10 +374,9 @@ export class ContextKeyNotEqualsExpr implements ContextKeyExpr { } public evaluate(context: IContext): boolean { - /* tslint:disable:triple-equals */ // Intentional != + // eslint-disable-next-line eqeqeq return (context.getValue(this.key) != this.value); - /* tslint:enable:triple-equals */ } public serialize(): string { diff --git a/src/vs/platform/contextkey/test/common/contextkey.test.ts b/src/vs/platform/contextkey/test/common/contextkey.test.ts index 2b37b058c4..be5ac957bb 100644 --- a/src/vs/platform/contextkey/test/common/contextkey.test.ts +++ b/src/vs/platform/contextkey/test/common/contextkey.test.ts @@ -65,7 +65,6 @@ suite('ContextKeyExpr', () => { }); test('evaluate', () => { - /* tslint:disable:triple-equals */ let context = createContext({ 'a': true, 'b': false, @@ -78,6 +77,7 @@ suite('ContextKeyExpr', () => { assert.equal(rules!.evaluate(context), expected, expr); } function testBatch(expr: string, value: any): void { + /* eslint-disable eqeqeq */ testExpression(expr, !!value); testExpression(expr + ' == true', !!value); testExpression(expr + ' != true', !value); @@ -92,6 +92,7 @@ suite('ContextKeyExpr', () => { testExpression(expr + ' >= 10', parseFloat(value) >= 10); testExpression(expr + ' <= 10', parseFloat(value) <= 10); // + /* eslint-enable eqeqeq */ } testBatch('a', true); @@ -102,9 +103,8 @@ suite('ContextKeyExpr', () => { testExpression('a && !b', true && !false); testExpression('a && b', true && false); - testExpression('a && !b && c == 5', true && !false && '5' == '5'); + testExpression('a && !b && c == 5', true && !false && '5' === '5'); testExpression('d =~ /e.*/', false); - /* tslint:enable:triple-equals */ // precedence test: false && true || true === true because && is evaluated first testExpression('b && a || a', true); diff --git a/src/vs/platform/dialogs/common/dialogs.ts b/src/vs/platform/dialogs/common/dialogs.ts index 4b2dbf7519..f165fc6fc9 100644 --- a/src/vs/platform/dialogs/common/dialogs.ts +++ b/src/vs/platform/dialogs/common/dialogs.ts @@ -230,9 +230,9 @@ export interface IFileDialogService { pickWorkspaceAndOpen(options: IPickAndOpenOptions): Promise; /** - * Shows a save file file dialog and save the file at the chosen file URI. + * Shows a save file dialog and save the file at the chosen file URI. */ - pickFileToSave(options: ISaveDialogOptions): Promise; + pickFileToSave(defaultUri: URI, availableFileSystems?: string[]): Promise; /** * Shows a save file dialog and returns the chosen file URI. @@ -257,9 +257,8 @@ export const enum ConfirmResult { } const MAX_CONFIRM_FILES = 10; -export function getConfirmMessage(start: string, fileNamesOrResources: readonly (string | URI)[]): string { - const message = [start]; - message.push(''); +export function getFileNamesMessage(fileNamesOrResources: readonly (string | URI)[]): string { + const message: string[] = []; message.push(...fileNamesOrResources.slice(0, MAX_CONFIRM_FILES).map(fileNameOrResource => typeof fileNameOrResource === 'string' ? fileNameOrResource : basename(fileNameOrResource))); if (fileNamesOrResources.length > MAX_CONFIRM_FILES) { diff --git a/src/vs/platform/dialogs/electron-main/dialogs.ts b/src/vs/platform/dialogs/electron-main/dialogs.ts index 1cf45f39a7..39f7c0d959 100644 --- a/src/vs/platform/dialogs/electron-main/dialogs.ts +++ b/src/vs/platform/dialogs/electron-main/dialogs.ts @@ -173,7 +173,7 @@ export class DialogMainService implements IDialogMainService { showOpenDialog(options: OpenDialogOptions, window?: BrowserWindow): Promise { - function normalizePaths(paths: string[] | undefined): string[] | undefined { + function normalizePaths(paths: string[]): string[] { if (paths && paths.length > 0 && isMacintosh) { paths = paths.map(path => normalizeNFC(path)); // normalize paths returned from the OS } diff --git a/src/vs/platform/driver/electron-main/driver.ts b/src/vs/platform/driver/electron-main/driver.ts index f5d4c92476..9ef9ab6fdb 100644 --- a/src/vs/platform/driver/electron-main/driver.ts +++ b/src/vs/platform/driver/electron-main/driver.ts @@ -18,7 +18,6 @@ import { ScanCodeBinding } from 'vs/base/common/scanCode'; import { KeybindingParser } from 'vs/base/common/keybindingParser'; import { timeout } from 'vs/base/common/async'; import { IDriver, IElement, IWindowDriver } from 'vs/platform/driver/common/driver'; -import { NativeImage } from 'electron'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { IElectronMainService } from 'vs/platform/electron/electron-main/electronMainService'; @@ -67,7 +66,7 @@ export class Driver implements IDriver, IWindowDriverRegistry { throw new Error('Invalid window'); } const webContents = window.win.webContents; - const image = await new Promise(c => webContents.capturePage(c)); + const image = await webContents.capturePage(); return image.toPNG().toString('base64'); } diff --git a/src/vs/platform/editor/common/editor.ts b/src/vs/platform/editor/common/editor.ts index b2b56fd823..ad447ed818 100644 --- a/src/vs/platform/editor/common/editor.ts +++ b/src/vs/platform/editor/common/editor.ts @@ -220,15 +220,15 @@ export interface ITextEditorOptions extends IEditorOptions { /** * Text editor selection. */ - selection?: ITextEditorSelection; + readonly selection?: ITextEditorSelection; /** * Text editor view state. */ - viewState?: object; + readonly viewState?: object; /** * Option to scroll vertically or horizontally as necessary and reveal a range centered vertically only if it lies outside the viewport. */ - revealInCenterIfOutsideViewport?: boolean; + readonly revealInCenterIfOutsideViewport?: boolean; } diff --git a/src/vs/platform/electron/electron-main/electronMainService.ts b/src/vs/platform/electron/electron-main/electronMainService.ts index b50795b045..487fc77b84 100644 --- a/src/vs/platform/electron/electron-main/electronMainService.ts +++ b/src/vs/platform/electron/electron-main/electronMainService.ts @@ -367,15 +367,13 @@ export class ElectronMainService implements IElectronMainService { //#region Connectivity async resolveProxy(windowId: number | undefined, url: string): Promise { - return new Promise(resolve => { - const window = this.windowById(windowId); - const session = window?.win?.webContents?.session; - if (session) { - session.resolveProxy(url, proxy => resolve(proxy)); - } else { - resolve(); - } - }); + const window = this.windowById(windowId); + const session = window?.win?.webContents?.session; + if (session) { + return session.resolveProxy(url); + } else { + return undefined; + } } //#endregion diff --git a/src/vs/platform/environment/node/stdin.ts b/src/vs/platform/environment/node/stdin.ts new file mode 100644 index 0000000000..401e1eaf54 --- /dev/null +++ b/src/vs/platform/environment/node/stdin.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +/** + * This code is also used by standalone cli's. Avoid adding dependencies to keep the size of the cli small. + */ +import * as paths from 'vs/base/common/path'; +import * as fs from 'fs'; +import * as os from 'os'; +import { resolveTerminalEncoding } from 'vs/base/node/terminalEncoding'; + +export function hasStdinWithoutTty() { + try { + return !process.stdin.isTTY; // Via https://twitter.com/MylesBorins/status/782009479382626304 + } catch (error) { + // Windows workaround for https://github.com/nodejs/node/issues/11656 + } + return false; +} + +export function stdinDataListener(durationinMs: number): Promise { + return new Promise(c => { + const dataListener = () => c(true); + + // wait for 1s maximum... + setTimeout(() => { + process.stdin.removeListener('data', dataListener); + + c(false); + }, durationinMs); + + // ...but finish early if we detect data + process.stdin.once('data', dataListener); + }); +} + +export function getStdinFilePath(): string { + return paths.join(os.tmpdir(), `code-stdin-${Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 3)}.txt`); +} + +export function readFromStdin(targetPath: string, verbose: boolean): Promise { + // open tmp file for writing + const stdinFileStream = fs.createWriteStream(targetPath); + // Pipe into tmp file using terminals encoding + return resolveTerminalEncoding(verbose).then(async encoding => { + + const iconv = await import('iconv-lite'); + if (!iconv.encodingExists(encoding)) { + console.log(`Unsupported terminal encoding: ${encoding}, falling back to UTF-8.`); + encoding = 'utf8'; + } + const converterStream = iconv.decodeStream(encoding); + process.stdin.pipe(converterStream).pipe(stdinFileStream); + }); +} diff --git a/src/vs/platform/files/common/fileService.ts b/src/vs/platform/files/common/fileService.ts index 9889bb49fd..77db8a890a 100644 --- a/src/vs/platform/files/common/fileService.ts +++ b/src/vs/platform/files/common/fileService.ts @@ -46,7 +46,7 @@ export class FileService extends Disposable implements IFileService { registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable { if (this.provider.has(scheme)) { - throw new Error(`A provider for the scheme ${scheme} is already registered.`); + throw new Error(`A filesystem provider for the scheme '${scheme}' is already registered.`); } // Add provider with event @@ -106,7 +106,7 @@ export class FileService extends Disposable implements IFileService { // Assert path is absolute if (!isAbsolutePath(resource)) { - throw new FileOperationError(localize('invalidPath', "The path of resource '{0}' must be absolute", this.resourceForError(resource)), FileOperationResult.FILE_INVALID_PATH); + throw new FileOperationError(localize('invalidPath', "Unable to resolve filesystem provider with relative file path '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_INVALID_PATH); } // Activate provider @@ -132,7 +132,7 @@ export class FileService extends Disposable implements IFileService { return provider; } - throw new Error(`Provider for scheme '${resource.scheme}' neither has FileReadWrite, FileReadStream nor FileOpenReadWriteClose capability which is needed for the read operation.`); + throw new Error(`Filesystem provider for scheme '${resource.scheme}' neither has FileReadWrite, FileReadStream nor FileOpenReadWriteClose capability which is needed for the read operation.`); } private async withWriteProvider(resource: URI): Promise { @@ -142,7 +142,7 @@ export class FileService extends Disposable implements IFileService { return provider; } - throw new Error(`Provider for scheme '${resource.scheme}' neither has FileReadWrite nor FileOpenReadWriteClose capability which is needed for the write operation.`); + throw new Error(`Filesystem provider for scheme '${resource.scheme}' neither has FileReadWrite nor FileOpenReadWriteClose capability which is needed for the write operation.`); } //#endregion @@ -164,7 +164,7 @@ export class FileService extends Disposable implements IFileService { // Specially handle file not found case as file operation result if (toFileSystemProviderErrorCode(error) === FileSystemProviderErrorCode.FileNotFound) { - throw new FileOperationError(localize('fileNotFoundError', "File not found ({0})", this.resourceForError(resource)), FileOperationResult.FILE_NOT_FOUND); + throw new FileOperationError(localize('fileNotFoundError', "Unable to resolve non-existing file '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_NOT_FOUND); } // Bubble up any other error as is @@ -292,7 +292,7 @@ export class FileService extends Disposable implements IFileService { // validate overwrite if (!options?.overwrite && await this.exists(resource)) { - throw new FileOperationError(localize('fileExists', "File to create already exists ({0})", this.resourceForError(resource)), FileOperationResult.FILE_MODIFIED_SINCE, options); + throw new FileOperationError(localize('fileExists', "Unable to create file '{0}' that already exists when overwrite flag is not set", this.resourceForError(resource)), FileOperationResult.FILE_MODIFIED_SINCE, options); } // do write into file (this will create it too) @@ -355,7 +355,7 @@ export class FileService extends Disposable implements IFileService { // file cannot be directory if ((stat.type & FileType.Directory) !== 0) { - throw new FileOperationError(localize('fileIsDirectoryError', "Expected file '{0}' is actually a directory", this.resourceForError(resource)), FileOperationResult.FILE_IS_DIRECTORY, options); + throw new FileOperationError(localize('fileIsDirectoryWriteError', "Unable to write file '{0}' that is actually a directory", this.resourceForError(resource)), FileOperationResult.FILE_IS_DIRECTORY, options); } // Dirty write prevention: if the file on disk has been changed and does not match our expected @@ -504,7 +504,7 @@ export class FileService extends Disposable implements IFileService { // Throw if resource is a directory if (stat.isDirectory) { - throw new FileOperationError(localize('fileIsDirectoryError', "Expected file '{0}' is actually a directory", this.resourceForError(resource)), FileOperationResult.FILE_IS_DIRECTORY, options); + throw new FileOperationError(localize('fileIsDirectoryReadError', "Unable to read file '{0}' that is actually a directory", this.resourceForError(resource)), FileOperationResult.FILE_IS_DIRECTORY, options); } // Throw if file not modified since (unless disabled) @@ -531,7 +531,7 @@ export class FileService extends Disposable implements IFileService { } if (typeof tooLargeErrorResult === 'number') { - throw new FileOperationError(localize('fileTooLargeError', "File '{0}' is too large to open", this.resourceForError(resource)), tooLargeErrorResult); + throw new FileOperationError(localize('fileTooLargeError', "Unable to read file '{0}' that is too large to open", this.resourceForError(resource)), tooLargeErrorResult); } } } @@ -693,7 +693,7 @@ export class FileService extends Disposable implements IFileService { // Bail out if target exists and we are not about to overwrite if (!overwrite) { - throw new FileOperationError(localize('unableToMoveCopyError3', "Unable to move/copy since a file already exists at destination."), FileOperationResult.FILE_MOVE_CONFLICT); + throw new FileOperationError(localize('unableToMoveCopyError3', "Unable to move/copy '{0}' because target '{1}' already exists at destination.", this.resourceForError(source), this.resourceForError(target)), FileOperationResult.FILE_MOVE_CONFLICT); } // Special case: if the target is a parent of the source, we cannot delete @@ -701,7 +701,7 @@ export class FileService extends Disposable implements IFileService { if (sourceProvider === targetProvider) { const isPathCaseSensitive = !!(sourceProvider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive); if (isEqualOrParent(source, target, !isPathCaseSensitive)) { - throw new Error(localize('unableToMoveCopyError4', "Unable to move/copy since a file would replace the folder it is contained in.")); + throw new Error(localize('unableToMoveCopyError4', "Unable to move/copy '{0}' into '{1}' since a file would replace the folder it is contained in.", this.resourceForError(source), this.resourceForError(target))); } } } @@ -730,7 +730,7 @@ export class FileService extends Disposable implements IFileService { try { const stat = await provider.stat(directory); if ((stat.type & FileType.Directory) === 0) { - throw new Error(localize('mkdirExistsError', "Path '{0}' already exists, but is not a directory", this.resourceForError(directory))); + throw new Error(localize('mkdirExistsError', "Unable to create folder '{0}' that already exists but is not a directory", this.resourceForError(directory))); } break; // we have hit a directory that exists -> good @@ -762,13 +762,13 @@ export class FileService extends Disposable implements IFileService { // Validate trash support const useTrash = !!options?.useTrash; if (useTrash && !(provider.capabilities & FileSystemProviderCapabilities.Trash)) { - throw new Error(localize('err.trash', "Provider for scheme '{0}' does not support trash.", resource.scheme)); + throw new Error(localize('deleteFailedTrashUnsupported', "Unable to delete file '{0}' via trash because provider does not support it.", this.resourceForError(resource))); } // Validate delete const exists = await this.exists(resource); if (!exists) { - throw new FileOperationError(localize('fileNotFoundError', "File not found ({0})", this.resourceForError(resource)), FileOperationResult.FILE_NOT_FOUND); + throw new FileOperationError(localize('deleteFailedNotFound', "Unable to delete non-existing file '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_NOT_FOUND); } // Validate recursive @@ -776,7 +776,7 @@ export class FileService extends Disposable implements IFileService { if (!recursive && exists) { const stat = await this.resolve(resource); if (stat.isDirectory && Array.isArray(stat.children) && stat.children.length > 0) { - throw new Error(localize('deleteFailed', "Unable to delete non-empty folder '{0}'.", this.resourceForError(resource))); + throw new Error(localize('deleteFailedNonEmptyFolder', "Unable to delete non-empty folder '{0}'.", this.resourceForError(resource))); } } @@ -1059,7 +1059,7 @@ export class FileService extends Disposable implements IFileService { protected throwIfFileSystemIsReadonly(provider: T, resource: URI): T { if (provider.capabilities & FileSystemProviderCapabilities.Readonly) { - throw new FileOperationError(localize('err.readonly', "Resource '{0}' can not be modified.", this.resourceForError(resource)), FileOperationResult.FILE_PERMISSION_DENIED); + throw new FileOperationError(localize('err.readonly', "Unable to modify readonly file '{0}'", this.resourceForError(resource)), FileOperationResult.FILE_PERMISSION_DENIED); } return provider; diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 4e6f0ceab9..a9ee6ca196 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -787,8 +787,6 @@ export const HotExitConfiguration = { ON_EXIT_AND_WINDOW_CLOSE: 'onExitAndWindowClose' }; -export const CONTENT_CHANGE_EVENT_BUFFER_DELAY = 1000; - export const FILES_ASSOCIATIONS_CONFIG = 'files.associations'; export const FILES_EXCLUDE_CONFIG = 'files.exclude'; diff --git a/src/vs/platform/files/node/watcher/nsfw/nsfwWatcherService.ts b/src/vs/platform/files/node/watcher/nsfw/nsfwWatcherService.ts index cf5324e194..c14f08cff4 100644 --- a/src/vs/platform/files/node/watcher/nsfw/nsfwWatcherService.ts +++ b/src/vs/platform/files/node/watcher/nsfw/nsfwWatcherService.ts @@ -100,6 +100,10 @@ export class NsfwWatcherService implements IWatcherService { } } + if (this._verboseLogging) { + this.log(`Start watching with nsfw: ${request.path}`); + } + nsfw(request.path, events => { for (const e of events) { // Logging diff --git a/src/vs/platform/files/node/watcher/unix/chokidarWatcherService.ts b/src/vs/platform/files/node/watcher/unix/chokidarWatcherService.ts index fb122db062..7ba4bd898c 100644 --- a/src/vs/platform/files/node/watcher/unix/chokidarWatcherService.ts +++ b/src/vs/platform/files/node/watcher/unix/chokidarWatcherService.ts @@ -105,15 +105,9 @@ export class ChokidarWatcherService implements IWatcherService { } private _watch(basePath: string, requests: IWatcherRequest[]): IWatcher { - if (this._verboseLogging) { - this.log(`Start watching: ${basePath}]`); - } const pollingInterval = this._pollingInterval || 5000; const usePolling = this._usePolling; - if (usePolling && this._verboseLogging) { - this.log(`Use polling instead of fs.watch: Polling interval ${pollingInterval} ms`); - } const watcherOpts: chokidar.WatchOptions = { ignoreInitial: true, @@ -155,6 +149,10 @@ export class ChokidarWatcherService implements IWatcherService { this.warn(`Watcher basePath does not match version on disk and was corrected (original: ${basePath}, real: ${realBasePath})`); } + if (this._verboseLogging) { + this.log(`Start watching with chockidar: ${realBasePath}, excludes: ${excludes.join(',')}, usePolling: ${usePolling ? 'true, interval ' + pollingInterval : 'false'}`); + } + let chokidarWatcher: chokidar.FSWatcher | null = chokidar.watch(realBasePath, watcherOpts); this._watcherCount++; diff --git a/src/vs/platform/instantiation/common/instantiationService.ts b/src/vs/platform/instantiation/common/instantiationService.ts index e5cae20e43..9ae950c86f 100644 --- a/src/vs/platform/instantiation/common/instantiationService.ts +++ b/src/vs/platform/instantiation/common/instantiationService.ts @@ -16,7 +16,7 @@ const _enableTracing = false; // PROXY // Ghetto-declare of the global Proxy object. This isn't the proper way // but allows us to run this code in the browser without IE11. -declare var Proxy: any; +declare const Proxy: any; const _canUseProxy = typeof Proxy === 'function'; class CyclicDependencyError extends Error { diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index d6e64ae482..0b2ff70d4a 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -687,7 +687,7 @@ export class TreeResourceNavigator2 extends Disposable { readonly onDidOpenResource: Event> = this._onDidOpenResource.event; constructor( - private tree: WorkbenchObjectTree | WorkbenchDataTree | WorkbenchAsyncDataTree | WorkbenchCompressibleAsyncDataTree, + private tree: WorkbenchObjectTree | WorkbenchCompressibleObjectTree | WorkbenchDataTree | WorkbenchAsyncDataTree | WorkbenchCompressibleAsyncDataTree, options?: IResourceResultsNavigationOptions2 ) { super(); diff --git a/src/vs/platform/log/common/fileLogService.ts b/src/vs/platform/log/common/fileLogService.ts index 625fbdd42d..2462a6b9cf 100644 --- a/src/vs/platform/log/common/fileLogService.ts +++ b/src/vs/platform/log/common/fileLogService.ts @@ -173,7 +173,7 @@ export class FileLoggerService extends Disposable implements ILoggerService { getLogger(resource: URI): ILogger { let logger = this.loggers.get(resource.toString()); if (!logger) { - logger = new BufferLogService, this.logService.getLevel(); + logger = new BufferLogService(this.logService.getLevel()); this.loggers.set(resource.toString(), logger); whenProviderRegistered(resource, this.fileService).then(() => (logger).logger = this.instantiationService.createInstance(FileLogService, basename(resource), resource, this.logService.getLevel())); } diff --git a/src/vs/platform/log/common/log.ts b/src/vs/platform/log/common/log.ts index 0a70019bc3..56b21b2d9f 100644 --- a/src/vs/platform/log/common/log.ts +++ b/src/vs/platform/log/common/log.ts @@ -10,6 +10,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { LoggerChannelClient } from 'vs/platform/log/common/logIpc'; import { URI } from 'vs/base/common/uri'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; export const ILogService = createServiceDecorator('logService'); export const ILoggerService = createServiceDecorator('loggerService'); @@ -223,40 +224,48 @@ export class ConsoleLogInMainService extends AbstractLogService implements ILogS trace(message: string, ...args: any[]): void { if (this.getLevel() <= LogLevel.Trace) { - this.client.consoleLog('trace', [message, ...args]); + this.client.consoleLog('trace', [this.extractMessage(message), ...args]); } } debug(message: string, ...args: any[]): void { if (this.getLevel() <= LogLevel.Debug) { - this.client.consoleLog('debug', [message, ...args]); + this.client.consoleLog('debug', [this.extractMessage(message), ...args]); } } info(message: string, ...args: any[]): void { if (this.getLevel() <= LogLevel.Info) { - this.client.consoleLog('info', [message, ...args]); + this.client.consoleLog('info', [this.extractMessage(message), ...args]); } } warn(message: string | Error, ...args: any[]): void { if (this.getLevel() <= LogLevel.Warning) { - this.client.consoleLog('warn', [message, ...args]); + this.client.consoleLog('warn', [this.extractMessage(message), ...args]); } } - error(message: string, ...args: any[]): void { + error(message: string | Error, ...args: any[]): void { if (this.getLevel() <= LogLevel.Error) { - this.client.consoleLog('error', [message, ...args]); + this.client.consoleLog('error', [this.extractMessage(message), ...args]); } } - critical(message: string, ...args: any[]): void { + critical(message: string | Error, ...args: any[]): void { if (this.getLevel() <= LogLevel.Critical) { - this.client.consoleLog('critical', [message, ...args]); + this.client.consoleLog('critical', [this.extractMessage(message), ...args]); } } + private extractMessage(msg: string | Error): string { + if (typeof msg === 'string') { + return msg; + } + + return toErrorMessage(msg, this.getLevel() <= LogLevel.Trace); + } + dispose(): void { // noop } diff --git a/src/vs/platform/log/common/logIpc.ts b/src/vs/platform/log/common/logIpc.ts index aff760b50c..f7ee76474b 100644 --- a/src/vs/platform/log/common/logIpc.ts +++ b/src/vs/platform/log/common/logIpc.ts @@ -77,6 +77,8 @@ export class FollowerLogService extends DelegatedLogService implements ILogServi } setLevel(level: LogLevel): void { + super.setLevel(level); + this.master.setLevel(level); } } diff --git a/src/vs/platform/product/common/productService.ts b/src/vs/platform/product/common/productService.ts index ca33f1a260..ec3f5cc5b8 100644 --- a/src/vs/platform/product/common/productService.ts +++ b/src/vs/platform/product/common/productService.ts @@ -108,13 +108,6 @@ export interface IProductConfiguration { readonly msftInternalDomains?: string[]; readonly linkProtectionTrustedDomains?: readonly string[]; - - readonly auth?: { - loginUrl: string; - tokenUrl: string; - redirectUrl: string; - clientId: string; - }; } export interface IExeBasedExtensionTip { diff --git a/src/vs/platform/progress/common/progress.ts b/src/vs/platform/progress/common/progress.ts index 0aa48cbee8..8927f79763 100644 --- a/src/vs/platform/progress/common/progress.ts +++ b/src/vs/platform/progress/common/progress.ts @@ -57,6 +57,7 @@ export interface IProgressNotificationOptions extends IProgressOptions { readonly location: ProgressLocation.Notification; readonly primaryActions?: ReadonlyArray; readonly secondaryActions?: ReadonlyArray; + delay?: number; } export interface IProgressWindowOptions extends IProgressOptions { diff --git a/src/vs/platform/quickinput/common/quickInput.ts b/src/vs/platform/quickinput/common/quickInput.ts index 73adf3269b..03fb58cb9f 100644 --- a/src/vs/platform/quickinput/common/quickInput.ts +++ b/src/vs/platform/quickinput/common/quickInput.ts @@ -38,7 +38,7 @@ export interface IQuickNavigateConfiguration { export interface IPickOptions { /** - * an optional string to show as place holder in the input box to guide the user what she picks on + * an optional string to show as placeholder in the input box to guide the user what she picks on */ placeHolder?: string; @@ -110,7 +110,7 @@ export interface IInputOptions { prompt?: string; /** - * an optional string to show as place holder in the input box to guide the user what to type + * an optional string to show as placeholder in the input box to guide the user what to type */ placeHolder?: string; @@ -131,6 +131,8 @@ export interface IQuickInput { title: string | undefined; + description: string | undefined; + step: number | undefined; totalSteps: number | undefined; diff --git a/src/vs/platform/remote/common/remoteAgentFileSystemChannel.ts b/src/vs/platform/remote/common/remoteAgentFileSystemChannel.ts index 6604584f85..604fac572e 100644 --- a/src/vs/platform/remote/common/remoteAgentFileSystemChannel.ts +++ b/src/vs/platform/remote/common/remoteAgentFileSystemChannel.ts @@ -15,6 +15,7 @@ import { OperatingSystem } from 'vs/base/common/platform'; import { newWriteableStream, ReadableStreamEvents, ReadableStreamEventPayload } from 'vs/base/common/stream'; import { CancellationToken } from 'vs/base/common/cancellation'; import { canceled } from 'vs/base/common/errors'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; export const REMOTE_FILE_SYSTEM_CHANNEL_NAME = 'remotefilesystem'; @@ -32,13 +33,13 @@ export class RemoteFileSystemProvider extends Disposable implements private readonly session: string = generateUuid(); private readonly _onDidChange = this._register(new Emitter()); - readonly onDidChangeFile: Event = this._onDidChange.event; + readonly onDidChangeFile = this._onDidChange.event; - private _onDidWatchErrorOccur: Emitter = this._register(new Emitter()); - readonly onDidErrorOccur: Event = this._onDidWatchErrorOccur.event; + private _onDidWatchErrorOccur = this._register(new Emitter()); + readonly onDidErrorOccur = this._onDidWatchErrorOccur.event; private readonly _onDidChangeCapabilities = this._register(new Emitter()); - readonly onDidChangeCapabilities: Event = this._onDidChangeCapabilities.event; + readonly onDidChangeCapabilities = this._onDidChangeCapabilities.event; private _capabilities!: FileSystemProviderCapabilities; get capabilities(): FileSystemProviderCapabilities { return this._capabilities; } @@ -117,14 +118,29 @@ export class RemoteFileSystemProvider extends Disposable implements // Reading as file stream goes through an event to the remote side const listener = this.channel.listen>('readFileStream', [resource, opts])(dataOrErrorOrEnd => { + + // data if (dataOrErrorOrEnd instanceof VSBuffer) { - - // data: forward into the stream stream.write(dataOrErrorOrEnd.buffer); - } else { + } - // error / end: always end the stream on errors too - stream.end(dataOrErrorOrEnd === 'end' ? undefined : dataOrErrorOrEnd); + // end or error + else { + if (dataOrErrorOrEnd === 'end') { + stream.end(); + } else { + + // Since we receive data through a IPC channel, it is likely + // that the error was not serialized, or only partially. To + // ensure our API use is correct, we convert the data to an + // error here to forward it properly. + let error = dataOrErrorOrEnd; + if (!(error instanceof Error)) { + error = new Error(toErrorMessage(error)); + } + + stream.end(error); + } // Signal to the remote side that we no longer listen listener.dispose(); diff --git a/src/vs/platform/remote/common/remoteAuthorityResolver.ts b/src/vs/platform/remote/common/remoteAuthorityResolver.ts index 9c67f2e477..5a80813eba 100644 --- a/src/vs/platform/remote/common/remoteAuthorityResolver.ts +++ b/src/vs/platform/remote/common/remoteAuthorityResolver.ts @@ -17,8 +17,13 @@ export interface ResolvedOptions { readonly extensionHostEnv?: { [key: string]: string | null }; } +export interface TunnelDescription { + remoteAddress: { port: number, host: string }; + localAddress: string; +} export interface TunnelInformation { - environmentTunnels?: { remoteAddress: { port: number, host: string }, localAddress: string }[]; + environmentTunnels?: TunnelDescription[]; + hideCandidatePorts?: boolean; } export interface ResolverResult { diff --git a/src/vs/platform/remote/common/tunnel.ts b/src/vs/platform/remote/common/tunnel.ts index a21943c877..8bccd69e12 100644 --- a/src/vs/platform/remote/common/tunnel.ts +++ b/src/vs/platform/remote/common/tunnel.ts @@ -20,7 +20,7 @@ export interface RemoteTunnel { export interface TunnelOptions { remoteAddress: { port: number, host: string }; - localPort?: number; + localAddressPort?: number; label?: string; } diff --git a/src/vs/platform/remote/common/tunnelService.ts b/src/vs/platform/remote/common/tunnelService.ts deleted file mode 100644 index 057adb70f0..0000000000 --- a/src/vs/platform/remote/common/tunnelService.ts +++ /dev/null @@ -1,26 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ITunnelService, RemoteTunnel, ITunnelProvider } from 'vs/platform/remote/common/tunnel'; -import { Event, Emitter } from 'vs/base/common/event'; -import { IDisposable } from 'vs/base/common/lifecycle'; - -export class NoOpTunnelService implements ITunnelService { - _serviceBrand: undefined; - - public readonly tunnels: Promise = Promise.resolve([]); - private _onTunnelOpened: Emitter = new Emitter(); - public onTunnelOpened: Event = this._onTunnelOpened.event; - private _onTunnelClosed: Emitter<{ host: string, port: number }> = new Emitter(); - public onTunnelClosed: Event<{ host: string, port: number }> = this._onTunnelClosed.event; - openTunnel(_remoteHost: string, _remotePort: number): Promise | undefined { - return undefined; - } - async closeTunnel(_remoteHost: string, _remotePort: number): Promise { - } - setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable { - throw new Error('Method not implemented.'); - } -} diff --git a/src/vs/platform/sign/node/signService.ts b/src/vs/platform/sign/node/signService.ts index ed68540eec..f16a76e17f 100644 --- a/src/vs/platform/sign/node/signService.ts +++ b/src/vs/platform/sign/node/signService.ts @@ -5,9 +5,8 @@ import { ISignService } from 'vs/platform/sign/common/sign'; -/* tslint:disable */ - declare module vsda { + // eslint-disable-next-line @typescript-eslint/class-name-casing export class signer { sign(arg: any): any; } diff --git a/src/vs/platform/storage/node/storageIpc.ts b/src/vs/platform/storage/node/storageIpc.ts index a29e152059..2c78b05d3c 100644 --- a/src/vs/platform/storage/node/storageIpc.ts +++ b/src/vs/platform/storage/node/storageIpc.ts @@ -100,7 +100,12 @@ export class GlobalStorageDatabaseChannel extends Disposable implements IServerC private serializeEvents(events: IStorageChangeEvent[]): ISerializableItemsChangeEvent { const items = new Map(); - events.forEach(event => items.set(event.key, this.storageMainService.get(event.key))); + events.forEach(event => { + const existing = this.storageMainService.get(event.key); + if (typeof existing === 'string') { + items.set(event.key, existing); + } + }); return { items: mapToSerializable(items) }; } diff --git a/src/vs/platform/theme/common/tokenClassificationRegistry.ts b/src/vs/platform/theme/common/tokenClassificationRegistry.ts index 2f79b511cc..5f85f6ccf9 100644 --- a/src/vs/platform/theme/common/tokenClassificationRegistry.ts +++ b/src/vs/platform/theme/common/tokenClassificationRegistry.ts @@ -71,11 +71,8 @@ export namespace TokenStyle { while ((match = expression.exec(fontStyle))) { switch (match[0]) { case 'bold': bold = true; break; - case '-bold': bold = false; break; case 'italic': italic = true; break; - case '-italic': italic = false; break; case 'underline': underline = true; break; - case '-underline': underline = false; break; } } } @@ -99,14 +96,13 @@ export interface TokenStyleDefaults { } export interface TokenStylingDefaultRule { - classification: TokenClassification; - matchScore: number; + match(classification: TokenClassification): number; + selector: TokenClassification; defaults: TokenStyleDefaults; } export interface TokenStylingRule { - classification: TokenClassification; - matchScore: number; + match(classification: TokenClassification): number; value: TokenStyle; } @@ -220,10 +216,10 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry { }, fontStyle: { type: 'string', - description: nls.localize('schema.token.fontStyle', 'Font style of the rule: \'italic\', \'bold\' or \'underline\', \'-italic\', \'-bold\' or \'-underline\'or a combination. The empty string unsets inherited settings.'), + description: nls.localize('schema.token.fontStyle', 'Font style of the rule: \'italic\', \'bold\' or \'underline\' or a combination. The empty string unsets inherited settings.'), pattern: fontStylePattern, - patternErrorMessage: nls.localize('schema.fontStyle.error', 'Font style must be \'italic\', \'bold\' or \'underline\' to set a style or \'-italic\', \'-bold\' or \'-underline\' to unset or a combination. The empty string unsets all styles.'), - defaultSnippets: [{ label: nls.localize('schema.token.fontStyle.none', 'None (clear inherited style)'), bodyText: '""' }, { body: 'italic' }, { body: 'bold' }, { body: 'underline' }, { body: '-italic' }, { body: '-bold' }, { body: '-underline' }, { body: 'italic bold' }, { body: 'italic underline' }, { body: 'bold underline' }, { body: 'italic bold underline' }] + patternErrorMessage: nls.localize('schema.fontStyle.error', 'Font style must be \'italic\', \'bold\' or \'underline\' or a combination. The empty string unsets all styles.'), + defaultSnippets: [{ label: nls.localize('schema.token.fontStyle.none', 'None (clear inherited style)'), bodyText: '""' }, { body: 'italic' }, { body: 'bold' }, { body: 'underline' }, { body: 'italic underline' }, { body: 'bold underline' }, { body: 'italic bold underline' }] } }, additionalProperties: false, @@ -279,16 +275,35 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry { return { type: tokenTypeDesc.num, modifiers: allModifierBits }; } - public getTokenStylingRule(classification: TokenClassification, value: TokenStyle): TokenStylingRule { - return { classification, matchScore: getTokenStylingScore(classification), value }; + + private newMatcher(selector: TokenClassification) { + const score = getTokenStylingScore(selector); + return (classification: TokenClassification) => { + const selectorType = selector.type; + if (selectorType !== TOKEN_TYPE_WILDCARD_NUM && selectorType !== classification.type) { + return -1; + } + const selectorModifier = selector.modifiers; + if ((classification.modifiers & selectorModifier) !== selectorModifier) { + return -1; + } + return score; + }; } - public registerTokenStyleDefault(classification: TokenClassification, defaults: TokenStyleDefaults): void { - this.tokenStylingDefaultRules.push({ classification, matchScore: getTokenStylingScore(classification), defaults }); + public getTokenStylingRule(selector: TokenClassification, value: TokenStyle): TokenStylingRule { + return { + match: this.newMatcher(selector), + value + }; + } + + public registerTokenStyleDefault(selector: TokenClassification, defaults: TokenStyleDefaults): void { + this.tokenStylingDefaultRules.push({ selector, match: this.newMatcher(selector), defaults }); } public deregisterTokenStyleDefault(classification: TokenClassification): void { - this.tokenStylingDefaultRules = this.tokenStylingDefaultRules.filter(r => !(r.classification.type === classification.type && r.classification.modifiers === classification.modifiers)); + this.tokenStylingDefaultRules = this.tokenStylingDefaultRules.filter(r => !(r.selector.type === classification.type && r.selector.modifiers === classification.modifiers)); } public deregisterTokenType(id: string): void { @@ -333,18 +348,6 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry { } -export function matchTokenStylingRule(themeSelector: TokenStylingRule | TokenStylingDefaultRule, classification: TokenClassification): number { - const selectorType = themeSelector.classification.type; - if (selectorType !== TOKEN_TYPE_WILDCARD_NUM && selectorType !== classification.type) { - return -1; - } - const selectorModifier = themeSelector.classification.modifiers; - if ((classification.modifiers & selectorModifier) !== selectorModifier) { - return -1; - } - return themeSelector.matchScore; -} - const tokenClassificationRegistry = new TokenClassificationRegistry(); platform.Registry.add(Extensions.TokenClassificationContribution, tokenClassificationRegistry); @@ -378,7 +381,7 @@ function registerDefaultClassifications(): void { registerTokenType('class', nls.localize('class', "Style for classes."), [['entity.name.class']], 'type'); registerTokenType('interface', nls.localize('interface', "Style for interfaces."), undefined, 'type'); registerTokenType('enum', nls.localize('enum', "Style for enums."), undefined, 'type'); - registerTokenType('parameterType', nls.localize('parameterType', "Style for parameter types."), undefined, 'type'); + registerTokenType('typeParameter', nls.localize('typeParameter', "Style for type parameters."), undefined, 'type'); registerTokenType('function', nls.localize('function', "Style for functions"), [['entity.name.function'], ['support.function']]); registerTokenType('member', nls.localize('member', "Style for member"), [['entity.name.function'], ['support.function']]); @@ -395,12 +398,12 @@ function registerDefaultClassifications(): void { tokenClassificationRegistry.registerTokenModifier('declaration', nls.localize('declaration', "Style for all symbol declarations."), undefined); tokenClassificationRegistry.registerTokenModifier('documentation', nls.localize('documentation', "Style to use for references in documentation."), undefined); - //tokenClassificationRegistry.registerTokenModifier('member', nls.localize('member', "Style to use for member functions, variables (fields) and types."), undefined); tokenClassificationRegistry.registerTokenModifier('static', nls.localize('static', "Style to use for symbols that are static."), undefined); tokenClassificationRegistry.registerTokenModifier('abstract', nls.localize('abstract', "Style to use for symbols that are abstract."), undefined); tokenClassificationRegistry.registerTokenModifier('deprecated', nls.localize('deprecated', "Style to use for symbols that are deprecated."), undefined); tokenClassificationRegistry.registerTokenModifier('modification', nls.localize('modification', "Style to use for write accesses."), undefined); tokenClassificationRegistry.registerTokenModifier('async', nls.localize('async', "Style to use for symbols that are async."), undefined); + tokenClassificationRegistry.registerTokenModifier('readonly', nls.localize('readonly', "Style to use for symbols that are readonly."), undefined); } export function getTokenClassificationRegistry(): ITokenClassificationRegistry { diff --git a/src/vs/platform/theme/electron-main/themeMainService.ts b/src/vs/platform/theme/electron-main/themeMainService.ts index 49a2b2c728..63dbcf942a 100644 --- a/src/vs/platform/theme/electron-main/themeMainService.ts +++ b/src/vs/platform/theme/electron-main/themeMainService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { isWindows, isMacintosh } from 'vs/base/common/platform'; -import { systemPreferences, ipcMain as ipc } from 'electron'; +import { ipcMain as ipc, nativeTheme } from 'electron'; import { IStateService } from 'vs/platform/state/node/state'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -42,14 +42,14 @@ export class ThemeMainService implements IThemeMainService { } getBackgroundColor(): string { - if (isWindows && systemPreferences.isInvertedColorScheme()) { + if (isWindows && nativeTheme.shouldUseInvertedColorScheme) { return DEFAULT_BG_HC_BLACK; } let background = this.stateService.getItem(THEME_BG_STORAGE_KEY, null); if (!background) { let baseTheme: string; - if (isWindows && systemPreferences.isInvertedColorScheme()) { + if (isWindows && nativeTheme.shouldUseInvertedColorScheme) { baseTheme = 'hc-black'; } else { baseTheme = this.stateService.getItem(THEME_STORAGE_KEY, 'vs-dark').split(' ')[0]; diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index aad0a32f06..f8cca34e60 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -50,6 +50,10 @@ export abstract class AbstractUpdateService implements IUpdateService { @IRequestService protected requestService: IRequestService, @ILogService protected logService: ILogService, ) { + if (!this.environmentService.isBuilt) { + return; // updates are never enabled when running out of sources + } + if (this.environmentService.disableUpdates) { this.logService.info('update#ctor - updates are disabled by the environment'); return; diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts new file mode 100644 index 0000000000..cf0c47d998 --- /dev/null +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { IFileService } from 'vs/platform/files/common/files'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { URI } from 'vs/base/common/uri'; +import { SyncSource } from 'vs/platform/userDataSync/common/userDataSync'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { joinPath } from 'vs/base/common/resources'; +import { toLocalISOString } from 'vs/base/common/date'; +import { ThrottledDelayer } from 'vs/base/common/async'; + +export abstract class AbstractSynchroniser extends Disposable { + + private readonly syncFolder: URI; + private cleanUpDelayer: ThrottledDelayer; + + constructor( + syncSource: SyncSource, + @IFileService protected readonly fileService: IFileService, + @IEnvironmentService environmentService: IEnvironmentService + ) { + super(); + this.syncFolder = joinPath(environmentService.userRoamingDataHome, '.sync', syncSource); + this.cleanUpDelayer = new ThrottledDelayer(50); + } + + protected async backupLocal(content: VSBuffer): Promise { + const resource = joinPath(this.syncFolder, toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')); + await this.fileService.writeFile(resource, content); + 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)).sort(); + const toDelete = all.slice(0, Math.max(0, all.length - 9)); + await Promise.all(toDelete.map(stat => this.fileService.del(stat.resource))); + } + } + +} diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index 8e2bf6fc2f..4fd9bed3e9 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -18,12 +18,15 @@ import { Queue } from 'vs/base/common/async'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { localize } from 'vs/nls'; import { merge } from 'vs/platform/userDataSync/common/extensionsMerge'; +import { isNonEmptyArray } from 'vs/base/common/arrays'; -export interface ISyncPreviewResult { +interface ISyncPreviewResult { readonly added: ISyncExtension[]; - readonly removed: ISyncExtension[]; + readonly removed: IExtensionIdentifier[]; readonly updated: ISyncExtension[]; readonly remote: ISyncExtension[] | null; + readonly remoteUserData: IUserData | null; + readonly skippedExtensions: ISyncExtension[]; } interface ILastSyncUserData extends IUserData { @@ -72,6 +75,61 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser } } + async pull(): Promise { + if (!this.configurationService.getValue('sync.enableExtensions')) { + this.logService.info('Extensions: Skipped pulling extensions as it is disabled.'); + return; + } + + this.stop(); + + try { + this.logService.info('Extensions: Started pulling extensions...'); + this.setStatus(SyncStatus.Syncing); + + const remoteUserData = await this.getRemoteUserData(); + + if (remoteUserData.content !== null) { + const localExtensions = await this.getLocalExtensions(); + const remoteExtensions: ISyncExtension[] = JSON.parse(remoteUserData.content); + const { added, updated, remote } = merge(localExtensions, remoteExtensions, [], [], this.getIgnoredExtensions()); + await this.apply({ added, removed: [], updated, remote, remoteUserData, skippedExtensions: [] }); + } + + // No remote exists to pull + else { + this.logService.info('Extensions: Remote extensions does not exist.'); + } + + this.logService.info('Extensions: Finished pulling extensions.'); + } finally { + this.setStatus(SyncStatus.Idle); + } + } + + async push(): Promise { + if (!this.configurationService.getValue('sync.enableExtensions')) { + this.logService.info('Extensions: Skipped pushing extensions as it is disabled.'); + return; + } + + this.stop(); + + try { + this.logService.info('Extensions: Started pushing extensions...'); + this.setStatus(SyncStatus.Syncing); + + const localExtensions = await this.getLocalExtensions(); + const { added, removed, updated, remote } = merge(localExtensions, null, null, [], this.getIgnoredExtensions()); + await this.apply({ added, removed, updated, remote, remoteUserData: null, skippedExtensions: [] }); + + this.logService.info('Extensions: Finished pushing extensions.'); + } finally { + this.setStatus(SyncStatus.Idle); + } + + } + async sync(): Promise { if (!this.configurationService.getValue('sync.enableExtensions')) { this.logService.trace('Extensions: Skipping synchronizing extensions as it is disabled.'); @@ -90,7 +148,8 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser this.setStatus(SyncStatus.Syncing); try { - await this.doSync(); + const previewResult = await this.getPreview(); + await this.apply(previewResult); } catch (e) { this.setStatus(SyncStatus.Idle); if (e instanceof UserDataSyncStoreError && e.code === UserDataSyncStoreErrorCode.Rejected) { @@ -108,6 +167,28 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser stop(): void { } + async hasPreviouslySynced(): Promise { + const lastSyncData = await this.getLastSyncUserData(); + return !!lastSyncData; + } + + async hasRemoteData(): Promise { + const remoteUserData = await this.getRemoteUserData(); + return remoteUserData.content !== null; + } + + async hasLocalData(): Promise { + try { + const localExtensions = await this.getLocalExtensions(); + if (isNonEmptyArray(localExtensions)) { + return true; + } + } catch (error) { + /* ignore error */ + } + return false; + } + removeExtension(identifier: IExtensionIdentifier): Promise { return this.replaceQueue.queue(async () => { const remoteData = await this.userDataSyncStoreService.read(ExtensionsSynchroniser.EXTERNAL_USER_DATA_EXTENSIONS_KEY, null); @@ -124,13 +205,19 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser }); } - private async doSync(): Promise { + async resetLocal(): Promise { + try { + await this.fileService.del(this.lastSyncExtensionsResource); + } catch (e) { /* ignore */ } + } + + private async getPreview(): Promise { const lastSyncData = await this.getLastSyncUserData(); const lastSyncExtensions: ISyncExtension[] | null = lastSyncData ? JSON.parse(lastSyncData.content!) : null; - let skippedExtensions: ISyncExtension[] = lastSyncData ? lastSyncData.skippedExtensions || [] : []; + const skippedExtensions: ISyncExtension[] = lastSyncData ? lastSyncData.skippedExtensions || [] : []; - let remoteData = await this.userDataSyncStoreService.read(ExtensionsSynchroniser.EXTERNAL_USER_DATA_EXTENSIONS_KEY, lastSyncData); - const remoteExtensions: ISyncExtension[] = remoteData.content ? JSON.parse(remoteData.content) : null; + const remoteUserData = await this.getRemoteUserData(lastSyncData); + const remoteExtensions: ISyncExtension[] = remoteUserData.content ? JSON.parse(remoteUserData.content) : null; const localExtensions = await this.getLocalExtensions(); @@ -140,9 +227,16 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser this.logService.info('Extensions: Remote extensions does not exist. Synchronizing extensions for the first time.'); } - const ignoredExtensions = this.configurationService.getValue('sync.ignoredExtensions') || []; - const { added, removed, updated, remote } = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, ignoredExtensions); + const { added, removed, updated, remote } = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, this.getIgnoredExtensions()); + return { added, removed, updated, remote, skippedExtensions, remoteUserData }; + } + + private getIgnoredExtensions() { + return this.configurationService.getValue('sync.ignoredExtensions') || []; + } + + private async apply({ added, removed, updated, remote, remoteUserData, skippedExtensions }: ISyncPreviewResult): Promise { if (!added.length && !removed.length && !updated.length && !remote) { this.logService.trace('Extensions: No changes found during synchronizing extensions.'); } @@ -155,15 +249,13 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser if (remote) { // update remote this.logService.info('Extensions: Updating remote extensions...'); - remoteData = await this.writeToRemote(remote, remoteData.ref); + remoteUserData = await this.writeToRemote(remote, remoteUserData ? remoteUserData.ref : null); } - if (remoteData.content - && (!lastSyncData || lastSyncData.ref !== remoteData.ref) - ) { + if (remoteUserData?.content) { // update last sync this.logService.info('Extensions: Updating last synchronised extensions...'); - await this.updateLastSyncValue({ ...remoteData, skippedExtensions }); + await this.updateLastSyncValue({ ...remoteUserData, skippedExtensions }); } } @@ -233,6 +325,10 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser await this.fileService.writeFile(this.lastSyncExtensionsResource, VSBuffer.fromString(JSON.stringify(lastSyncUserData))); } + private getRemoteUserData(lastSyncData?: IUserData | null): Promise { + return this.userDataSyncStoreService.read(ExtensionsSynchroniser.EXTERNAL_USER_DATA_EXTENSIONS_KEY, lastSyncData || null); + } + private async writeToRemote(extensions: ISyncExtension[], ref: string | null): Promise { const content = JSON.stringify(extensions); ref = await this.userDataSyncStoreService.write(ExtensionsSynchroniser.EXTERNAL_USER_DATA_EXTENSIONS_KEY, content, ref); diff --git a/src/vs/platform/userDataSync/common/globalStateMerge.ts b/src/vs/platform/userDataSync/common/globalStateMerge.ts new file mode 100644 index 0000000000..b7f7f4ba79 --- /dev/null +++ b/src/vs/platform/userDataSync/common/globalStateMerge.ts @@ -0,0 +1,85 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as objects from 'vs/base/common/objects'; +import { IGlobalState } from 'vs/platform/userDataSync/common/userDataSync'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { values } from 'vs/base/common/map'; + +export function merge(localGloablState: IGlobalState, remoteGlobalState: IGlobalState | null, lastSyncGlobalState: IGlobalState | null): { local?: IGlobalState, remote?: IGlobalState } { + if (!remoteGlobalState) { + return { remote: localGloablState }; + } + + const { local: localArgv, remote: remoteArgv } = doMerge(localGloablState.argv, remoteGlobalState.argv, lastSyncGlobalState ? lastSyncGlobalState.argv : null); + const { local: localStorage, remote: remoteStorage } = doMerge(localGloablState.storage, remoteGlobalState.storage, lastSyncGlobalState ? lastSyncGlobalState.storage : null); + const local: IGlobalState | undefined = localArgv || localStorage ? { argv: localArgv || localGloablState.argv, storage: localStorage || localGloablState.storage } : undefined; + const remote: IGlobalState | undefined = remoteArgv || remoteStorage ? { argv: remoteArgv || remoteGlobalState.argv, storage: remoteStorage || remoteGlobalState.storage } : undefined; + + return { local, remote }; +} + +function doMerge(local: IStringDictionary, remote: IStringDictionary, base: IStringDictionary | null): { local?: IStringDictionary, remote?: IStringDictionary } { + const localToRemote = compare(local, remote); + if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) { + // No changes found between local and remote. + return {}; + } + + const baseToRemote = base ? compare(base, remote) : { added: Object.keys(remote).reduce((r, k) => { r.add(k); return r; }, new Set()), removed: new Set(), updated: new Set() }; + if (baseToRemote.added.size === 0 && baseToRemote.removed.size === 0 && baseToRemote.updated.size === 0) { + // No changes found between base and remote. + return { remote: local }; + } + + const baseToLocal = base ? compare(base, local) : { added: Object.keys(local).reduce((r, k) => { r.add(k); return r; }, new Set()), removed: new Set(), updated: new Set() }; + if (baseToLocal.added.size === 0 && baseToLocal.removed.size === 0 && baseToLocal.updated.size === 0) { + // No changes found between base and local. + return { local: remote }; + } + + const merged = objects.deepClone(local); + + // Added in remote + for (const key of values(baseToRemote.added)) { + merged[key] = remote[key]; + } + + // Updated in Remote + for (const key of values(baseToRemote.updated)) { + merged[key] = remote[key]; + } + + // Removed in remote & local + for (const key of values(baseToRemote.removed)) { + // Got removed in local + if (baseToLocal.removed.has(key)) { + delete merged[key]; + } + } + + return { local: merged, remote: merged }; +} + +function compare(from: IStringDictionary, to: IStringDictionary): { added: Set, removed: Set, updated: Set } { + const fromKeys = Object.keys(from); + const toKeys = Object.keys(to); + const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set()); + const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set()); + const updated: Set = new Set(); + + for (const key of fromKeys) { + if (removed.has(key)) { + continue; + } + const value1 = from[key]; + const value2 = to[key]; + if (!objects.equals(value1, value2)) { + updated.add(key); + } + } + + return { added, removed, updated }; +} diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts new file mode 100644 index 0000000000..b1646c162d --- /dev/null +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -0,0 +1,259 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { IUserData, UserDataSyncStoreError, UserDataSyncStoreErrorCode, ISynchroniser, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState } from 'vs/platform/userDataSync/common/userDataSync'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { URI } from 'vs/base/common/uri'; +import { joinPath, dirname } from 'vs/base/common/resources'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { edit } from 'vs/platform/userDataSync/common/content'; +import { merge } from 'vs/platform/userDataSync/common/globalStateMerge'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { parse } from 'vs/base/common/json'; + +const argvProperties: string[] = ['locale']; + +interface ISyncPreviewResult { + readonly local: IGlobalState | undefined; + readonly remote: IGlobalState | undefined; + readonly remoteUserData: IUserData | null; +} + +export class GlobalStateSynchroniser extends Disposable implements ISynchroniser { + + private static EXTERNAL_USER_DATA_GLOBAL_STATE_KEY: string = 'globalState'; + + private _status: SyncStatus = SyncStatus.Idle; + get status(): SyncStatus { return this._status; } + private _onDidChangStatus: Emitter = this._register(new Emitter()); + readonly onDidChangeStatus: Event = this._onDidChangStatus.event; + + private _onDidChangeLocal: Emitter = this._register(new Emitter()); + readonly onDidChangeLocal: Event = this._onDidChangeLocal.event; + + private readonly lastSyncGlobalStateResource: URI; + + constructor( + @IFileService private readonly fileService: IFileService, + @IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService, + @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IConfigurationService private readonly configurationService: IConfigurationService, + ) { + super(); + this.lastSyncGlobalStateResource = joinPath(environmentService.userRoamingDataHome, '.lastSyncGlobalState'); + this._register(this.fileService.watch(dirname(this.environmentService.argvResource))); + this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.environmentService.argvResource))(() => this._onDidChangeLocal.fire())); + } + + private setStatus(status: SyncStatus): void { + if (this._status !== status) { + this._status = status; + this._onDidChangStatus.fire(status); + } + } + + async pull(): Promise { + if (!this.configurationService.getValue('sync.enableUIState')) { + this.logService.info('UI State: Skipped pulling ui state as it is disabled.'); + return; + } + + this.stop(); + + try { + this.logService.info('UI State: Started pulling ui state...'); + this.setStatus(SyncStatus.Syncing); + + const remoteUserData = await this.getRemoteUserData(); + + if (remoteUserData.content !== null) { + const local: IGlobalState = JSON.parse(remoteUserData.content); + await this.apply({ local, remote: undefined, remoteUserData }); + } + + // No remote exists to pull + else { + this.logService.info('UI State: Remote UI state does not exist.'); + } + + this.logService.info('UI State: Finished pulling UI state.'); + } finally { + this.setStatus(SyncStatus.Idle); + } + } + + async push(): Promise { + if (!this.configurationService.getValue('sync.enableUIState')) { + this.logService.info('UI State: Skipped pushing UI State as it is disabled.'); + return; + } + + this.stop(); + + try { + this.logService.info('UI State: Started pushing UI State...'); + this.setStatus(SyncStatus.Syncing); + + const remote = await this.getLocalGlobalState(); + await this.apply({ local: undefined, remote, remoteUserData: null }); + + this.logService.info('UI State: Finished pushing UI State.'); + } finally { + this.setStatus(SyncStatus.Idle); + } + + } + + async sync(): Promise { + if (!this.configurationService.getValue('sync.enableUIState')) { + this.logService.trace('UI State: Skipping synchronizing UI state as it is disabled.'); + return false; + } + + if (this.status !== SyncStatus.Idle) { + this.logService.trace('UI State: Skipping synchronizing ui state as it is running already.'); + return false; + } + + this.logService.trace('UI State: Started synchronizing ui state...'); + this.setStatus(SyncStatus.Syncing); + + try { + const result = await this.getPreview(); + await this.apply(result); + this.logService.trace('UI State: Finised synchronizing ui state.'); + return true; + } catch (e) { + this.setStatus(SyncStatus.Idle); + if (e instanceof UserDataSyncStoreError && e.code === UserDataSyncStoreErrorCode.Rejected) { + // Rejected as there is a new remote version. Syncing again, + this.logService.info('UI State: Failed to synchronise ui state as there is a new remote version available. Synchronizing again...'); + return this.sync(); + } + throw e; + } finally { + this.setStatus(SyncStatus.Idle); + } + } + + stop(): void { } + + async hasPreviouslySynced(): Promise { + const lastSyncData = await this.getLastSyncUserData(); + return !!lastSyncData; + } + + async hasRemoteData(): Promise { + const remoteUserData = await this.getRemoteUserData(); + return remoteUserData.content !== null; + } + + async hasLocalData(): Promise { + try { + const localGloablState = await this.getLocalGlobalState(); + if (localGloablState.argv['locale'] !== 'en') { + return true; + } + } catch (error) { + /* ignore error */ + } + return false; + } + + async resetLocal(): Promise { + try { + await this.fileService.del(this.lastSyncGlobalStateResource); + } catch (e) { /* ignore */ } + } + + private async getPreview(): Promise { + const lastSyncData = await this.getLastSyncUserData(); + const lastSyncGlobalState = lastSyncData && lastSyncData.content ? JSON.parse(lastSyncData.content) : null; + + const remoteUserData = await this.getRemoteUserData(); + const remoteGlobalState: IGlobalState = remoteUserData.content ? JSON.parse(remoteUserData.content) : null; + + const localGloablState = await this.getLocalGlobalState(); + + const { local, remote } = merge(localGloablState, remoteGlobalState, lastSyncGlobalState); + + return { local, remote, remoteUserData }; + } + + private async apply({ local, remote, remoteUserData }: ISyncPreviewResult): Promise { + if (local) { + // update local + this.logService.info('UI State: Updating local ui state...'); + await this.writeLocalGlobalState(local); + } + + if (remote) { + // update remote + this.logService.info('UI State: Updating remote ui state...'); + remoteUserData = await this.writeToRemote(remote, remoteUserData ? remoteUserData.ref : null); + } + + if (remoteUserData?.content) { + // update last sync + this.logService.info('UI State: Updating last synchronised ui state...'); + await this.updateLastSyncValue(remoteUserData); + } + } + + private async getLocalGlobalState(): Promise { + const argv: IStringDictionary = {}; + const storage: IStringDictionary = {}; + try { + const content = await this.fileService.readFile(this.environmentService.argvResource); + const argvValue: IStringDictionary = parse(content.value.toString()); + for (const argvProperty of argvProperties) { + if (argvValue[argvProperty] !== undefined) { + argv[argvProperty] = argvValue[argvProperty]; + } + } + } catch (error) { } + return { argv, storage }; + } + + private async writeLocalGlobalState(globalState: IGlobalState): Promise { + const content = await this.fileService.readFile(this.environmentService.argvResource); + let argvContent = content.value.toString(); + for (const argvProperty of Object.keys(globalState.argv)) { + argvContent = edit(argvContent, [argvProperty], globalState.argv[argvProperty], {}); + } + if (argvContent !== content.value.toString()) { + await this.fileService.writeFile(this.environmentService.argvResource, VSBuffer.fromString(argvContent)); + } + } + + private async getLastSyncUserData(): Promise { + try { + const content = await this.fileService.readFile(this.lastSyncGlobalStateResource); + return JSON.parse(content.value.toString()); + } catch (error) { + return null; + } + } + + private async updateLastSyncValue(remoteUserData: IUserData): Promise { + await this.fileService.writeFile(this.lastSyncGlobalStateResource, VSBuffer.fromString(JSON.stringify(remoteUserData))); + } + + private getRemoteUserData(lastSyncData?: IUserData | null): Promise { + return this.userDataSyncStoreService.read(GlobalStateSynchroniser.EXTERNAL_USER_DATA_GLOBAL_STATE_KEY, lastSyncData || null); + } + + private async writeToRemote(globalState: IGlobalState, ref: string | null): Promise { + const content = JSON.stringify(globalState); + ref = await this.userDataSyncStoreService.write(GlobalStateSynchroniser.EXTERNAL_USER_DATA_GLOBAL_STATE_KEY, content, ref); + return { content, ref }; + } + +} diff --git a/src/vs/platform/userDataSync/common/keybindingsSync.ts b/src/vs/platform/userDataSync/common/keybindingsSync.ts index 8e173d109d..84597d08ff 100644 --- a/src/vs/platform/userDataSync/common/keybindingsSync.ts +++ b/src/vs/platform/userDataSync/common/keybindingsSync.ts @@ -3,9 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; -import { IFileService, FileSystemProviderErrorCode, FileSystemProviderError, IFileContent } from 'vs/platform/files/common/files'; -import { IUserData, UserDataSyncStoreError, UserDataSyncStoreErrorCode, ISynchroniser, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IFileService, FileSystemProviderErrorCode, FileSystemProviderError, IFileContent, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; +import { IUserData, UserDataSyncStoreError, UserDataSyncStoreErrorCode, ISynchroniser, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, SyncSource } from 'vs/platform/userDataSync/common/userDataSync'; import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge'; import { VSBuffer } from 'vs/base/common/buffer'; import { parse, ParseError } from 'vs/base/common/json'; @@ -20,6 +19,8 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { OS, OperatingSystem } from 'vs/base/common/platform'; import { isUndefined } from 'vs/base/common/types'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; +import { isNonEmptyArray } from 'vs/base/common/arrays'; +import { AbstractSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer'; interface ISyncContent { mac?: string; @@ -30,13 +31,13 @@ interface ISyncContent { interface ISyncPreviewResult { readonly fileContent: IFileContent | null; - readonly remoteUserData: IUserData; + readonly remoteUserData: IUserData | null; readonly hasLocalChanged: boolean; readonly hasRemoteChanged: boolean; readonly hasConflicts: boolean; } -export class KeybindingsSynchroniser extends Disposable implements ISynchroniser { +export class KeybindingsSynchroniser extends AbstractSynchroniser implements ISynchroniser { private static EXTERNAL_USER_DATA_KEYBINDINGS_KEY: string = 'keybindings'; @@ -56,11 +57,11 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser @IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IFileService private readonly fileService: IFileService, + @IFileService fileService: IFileService, @IEnvironmentService private readonly environmentService: IEnvironmentService, @IUserDataSyncUtilService private readonly userDataSyncUtilService: IUserDataSyncUtilService, ) { - super(); + super(SyncSource.Keybindings, fileService, environmentService); this.lastSyncKeybindingsResource = joinPath(this.environmentService.userRoamingDataHome, '.lastSyncKeybindings.json'); this._register(this.fileService.watch(dirname(this.environmentService.keybindingsResource))); this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.environmentService.keybindingsResource))(() => this._onDidChangeLocal.fire())); @@ -73,6 +74,84 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser } } + async pull(): Promise { + if (!this.configurationService.getValue('sync.enableKeybindings')) { + this.logService.info('Keybindings: Skipped pulling keybindings as it is disabled.'); + return; + } + + this.stop(); + + try { + this.logService.info('Keybindings: Started pulling keybindings...'); + this.setStatus(SyncStatus.Syncing); + + const remoteUserData = await this.getRemoteUserData(); + const remoteContent = remoteUserData.content !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.content) : null; + + if (remoteContent !== null) { + await this.fileService.writeFile(this.environmentService.keybindingsSyncPreviewResource, VSBuffer.fromString(remoteContent)); + const fileContent = await this.getLocalFileContent(); + this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve({ + fileContent, + hasConflicts: false, + hasLocalChanged: true, + hasRemoteChanged: false, + remoteUserData + })); + await this.apply(); + } + + // No remote exists to pull + else { + this.logService.info('Keybindings: Remote keybindings does not exist.'); + } + + this.logService.info('Keybindings: Finished pulling keybindings.'); + } finally { + this.setStatus(SyncStatus.Idle); + } + + } + + async push(): Promise { + if (!this.configurationService.getValue('sync.enableKeybindings')) { + this.logService.info('Keybindings: Skipped pushing keybindings as it is disabled.'); + return; + } + + this.stop(); + + try { + this.logService.info('Keybindings: Started pushing keybindings...'); + this.setStatus(SyncStatus.Syncing); + + const fileContent = await this.getLocalFileContent(); + + if (fileContent !== null) { + await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, fileContent.value); + this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve({ + fileContent, + hasConflicts: false, + hasLocalChanged: false, + hasRemoteChanged: true, + remoteUserData: null + })); + await this.apply(); + } + + // No local exists to push + else { + this.logService.info('Keybindings: Local keybindings does not exist.'); + } + + this.logService.info('Keybindings: Finished pushing keybindings.'); + } finally { + this.setStatus(SyncStatus.Idle); + } + + } + async sync(_continue?: boolean): Promise { if (!this.configurationService.getValue('sync.enableKeybindings')) { this.logService.trace('Keybindings: Skipping synchronizing keybindings as it is disabled.'); @@ -99,8 +178,13 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser this.setStatus(SyncStatus.HasConflicts); return false; } - await this.apply(); - return true; + try { + await this.apply(); + this.logService.trace('Keybindings: Finished synchronizing keybindings...'); + return true; + } finally { + this.setStatus(SyncStatus.Idle); + } } catch (e) { this.syncPreviewResultPromise = null; this.setStatus(SyncStatus.Idle); @@ -128,11 +212,45 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser this.setStatus(SyncStatus.Idle); } + async hasPreviouslySynced(): Promise { + const lastSyncData = await this.getLastSyncUserData(); + return !!lastSyncData; + } + + async hasRemoteData(): Promise { + const remoteUserData = await this.getRemoteUserData(); + return remoteUserData.content !== null; + } + + async hasLocalData(): Promise { + try { + const localFileContent = await this.getLocalFileContent(); + if (localFileContent) { + const keybindings = parse(localFileContent.value.toString()); + if (isNonEmptyArray(keybindings)) { + return true; + } + } + } catch (error) { + if ((error).fileOperationResult !== FileOperationResult.FILE_NOT_FOUND) { + return true; + } + } + return false; + } + + async resetLocal(): Promise { + try { + await this.fileService.del(this.lastSyncKeybindingsResource); + } catch (e) { /* ignore */ } + } + private async continueSync(): Promise { if (this.status !== SyncStatus.HasConflicts) { return false; } await this.apply(); + this.setStatus(SyncStatus.Idle); return true; } @@ -160,11 +278,12 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser } if (hasRemoteChanged) { this.logService.info('Keybindings: Updating remote keybindings'); - const remoteContents = this.updateSyncContent(content, remoteUserData.content); - const ref = await this.updateRemoteUserData(remoteContents, remoteUserData.ref); + let remoteContents = remoteUserData ? remoteUserData.content : (await this.getRemoteUserData()).content; + remoteContents = this.updateSyncContent(content, remoteContents); + const ref = await this.updateRemoteUserData(remoteContents, remoteUserData ? remoteUserData.ref : null); remoteUserData = { ref, content: remoteContents }; } - if (remoteUserData.content) { + if (remoteUserData?.content) { this.logService.info('Keybindings: Updating last synchronised keybindings'); const lastSyncContent = this.updateSyncContent(content, null); await this.updateLastSyncUserData({ ref: remoteUserData.ref, content: lastSyncContent }); @@ -176,9 +295,7 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser this.logService.trace('Keybindings: No changes found during synchronizing keybindings.'); } - this.logService.trace('Keybindings: Finised synchronizing keybindings.'); this.syncPreviewResultPromise = null; - this.setStatus(SyncStatus.Idle); } private hasErrors(content: string): boolean { @@ -200,7 +317,7 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser const remoteUserData = await this.getRemoteUserData(lastSyncData); const remoteContent = remoteUserData.content ? this.getKeybindingsContentFromSyncContent(remoteUserData.content) : null; // Get file content last to get the latest - const fileContent = await this.getLocalContent(); + const fileContent = await this.getLocalFileContent(); let hasLocalChanged: boolean = false; let hasRemoteChanged: boolean = false; let hasConflicts: boolean = false; @@ -252,7 +369,7 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser return this._formattingOptions; } - private async getLocalContent(): Promise { + private async getLocalFileContent(): Promise { try { return await this.fileService.readFile(this.environmentService.keybindingsResource); } catch (error) { @@ -263,6 +380,7 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser private async updateLocalContent(newContent: string, oldContent: IFileContent | null): Promise { if (oldContent) { // file exists already + await this.backupLocal(oldContent.value); await this.fileService.writeFile(this.environmentService.keybindingsResource, VSBuffer.fromString(newContent), oldContent); } else { // file does not exist @@ -283,8 +401,8 @@ export class KeybindingsSynchroniser extends Disposable implements ISynchroniser await this.fileService.writeFile(this.lastSyncKeybindingsResource, VSBuffer.fromString(JSON.stringify(remoteUserData))); } - private async getRemoteUserData(lastSyncData: IUserData | null): Promise { - return this.userDataSyncStoreService.read(KeybindingsSynchroniser.EXTERNAL_USER_DATA_KEYBINDINGS_KEY, lastSyncData); + private async getRemoteUserData(lastSyncData?: IUserData | null): Promise { + return this.userDataSyncStoreService.read(KeybindingsSynchroniser.EXTERNAL_USER_DATA_KEYBINDINGS_KEY, lastSyncData || null); } private async updateRemoteUserData(content: string, ref: string | null): Promise { diff --git a/src/vs/platform/userDataSync/common/settingsMerge.ts b/src/vs/platform/userDataSync/common/settingsMerge.ts index 5274945839..dc55153576 100644 --- a/src/vs/platform/userDataSync/common/settingsMerge.ts +++ b/src/vs/platform/userDataSync/common/settingsMerge.ts @@ -10,21 +10,19 @@ import { values } from 'vs/base/common/map'; import { IStringDictionary } from 'vs/base/common/collections'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; import * as contentUtil from 'vs/platform/userDataSync/common/content'; +import { IConflictSetting } from 'vs/platform/userDataSync/common/userDataSync'; -export function computeRemoteContent(localContent: string, remoteContent: string, ignoredSettings: string[], formattingOptions: FormattingOptions): string { +export function updateIgnoredSettings(targetContent: string, sourceContent: string, ignoredSettings: string[], formattingOptions: FormattingOptions): string { if (ignoredSettings.length) { - const remote = parse(remoteContent); - const ignored = ignoredSettings.reduce((set, key) => { set.add(key); return set; }, new Set()); + const source = parse(sourceContent); for (const key of ignoredSettings) { - if (ignored.has(key)) { - localContent = contentUtil.edit(localContent, [key], remote[key], formattingOptions); - } + targetContent = contentUtil.edit(targetContent, [key], source[key], formattingOptions); } } - return localContent; + return targetContent; } -export function merge(localContent: string, remoteContent: string, baseContent: string | null, ignoredSettings: string[], formattingOptions: FormattingOptions): { mergeContent: string, hasChanges: boolean, hasConflicts: boolean } { +export function merge(localContent: string, remoteContent: string, baseContent: string | null, ignoredSettings: string[], resolvedConflicts: { key: string, value: any | undefined }[], formattingOptions: FormattingOptions): { mergeContent: string, hasChanges: boolean, conflicts: IConflictSetting[] } { const local = parse(localContent); const remote = parse(remoteContent); const base = baseContent ? parse(baseContent) : null; @@ -33,30 +31,41 @@ export function merge(localContent: string, remoteContent: string, baseContent: const localToRemote = compare(local, remote, ignored); if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) { // No changes found between local and remote. - return { mergeContent: localContent, hasChanges: false, hasConflicts: false }; + return { mergeContent: localContent, hasChanges: false, conflicts: [] }; } - const conflicts: Set = new Set(); + const conflicts: Map = new Map(); + const handledConflicts: Set = new Set(); const baseToLocal = base ? compare(base, local, ignored) : { added: Object.keys(local).reduce((r, k) => { r.add(k); return r; }, new Set()), removed: new Set(), updated: new Set() }; const baseToRemote = base ? compare(base, remote, ignored) : { added: Object.keys(remote).reduce((r, k) => { r.add(k); return r; }, new Set()), removed: new Set(), updated: new Set() }; let mergeContent = localContent; + const handleConflict = (conflictKey: string): void => { + handledConflicts.add(conflictKey); + const resolvedConflict = resolvedConflicts.filter(({ key }) => key === conflictKey)[0]; + if (resolvedConflict) { + mergeContent = contentUtil.edit(mergeContent, [conflictKey], resolvedConflict.value, formattingOptions); + } else { + conflicts.set(conflictKey, { key: conflictKey, localValue: local[conflictKey], remoteValue: remote[conflictKey] }); + } + }; + // Removed settings in Local for (const key of values(baseToLocal.removed)) { // Got updated in remote if (baseToRemote.updated.has(key)) { - conflicts.add(key); + handleConflict(key); } } // Removed settings in Remote for (const key of values(baseToRemote.removed)) { - if (conflicts.has(key)) { + if (handledConflicts.has(key)) { continue; } // Got updated in local if (baseToLocal.updated.has(key)) { - conflicts.add(key); + handleConflict(key); } else { mergeContent = contentUtil.edit(mergeContent, [key], undefined, formattingOptions); } @@ -64,28 +73,28 @@ export function merge(localContent: string, remoteContent: string, baseContent: // Added settings in Local for (const key of values(baseToLocal.added)) { - if (conflicts.has(key)) { + if (handledConflicts.has(key)) { continue; } // Got added in remote if (baseToRemote.added.has(key)) { // Has different value if (localToRemote.updated.has(key)) { - conflicts.add(key); + handleConflict(key); } } } // Added settings in remote for (const key of values(baseToRemote.added)) { - if (conflicts.has(key)) { + if (handledConflicts.has(key)) { continue; } // Got added in local if (baseToLocal.added.has(key)) { // Has different value if (localToRemote.updated.has(key)) { - conflicts.add(key); + handleConflict(key); } } else { mergeContent = contentUtil.edit(mergeContent, [key], remote[key], formattingOptions); @@ -94,28 +103,28 @@ export function merge(localContent: string, remoteContent: string, baseContent: // Updated settings in Local for (const key of values(baseToLocal.updated)) { - if (conflicts.has(key)) { + if (handledConflicts.has(key)) { continue; } // Got updated in remote if (baseToRemote.updated.has(key)) { // Has different value if (localToRemote.updated.has(key)) { - conflicts.add(key); + handleConflict(key); } } } // Updated settings in Remote for (const key of values(baseToRemote.updated)) { - if (conflicts.has(key)) { + if (handledConflicts.has(key)) { continue; } // Got updated in local if (baseToLocal.updated.has(key)) { // Has different value if (localToRemote.updated.has(key)) { - conflicts.add(key); + handleConflict(key); } } else { mergeContent = contentUtil.edit(mergeContent, [key], remote[key], formattingOptions); @@ -126,7 +135,7 @@ export function merge(localContent: string, remoteContent: string, baseContent: const conflictNodes: { key: string, node: Node | undefined }[] = []; const tree = parseTree(mergeContent); const eol = formattingOptions.eol!; - for (const key of values(conflicts)) { + for (const { key } of values(conflicts)) { const node = findNodeAtLocation(tree, [key]); conflictNodes.push({ key, node }); } @@ -166,7 +175,7 @@ export function merge(localContent: string, remoteContent: string, baseContent: } } - return { mergeContent, hasChanges: true, hasConflicts: conflicts.size > 0 }; + return { mergeContent, hasChanges: true, conflicts: values(conflicts) }; } function compare(from: IStringDictionary, to: IStringDictionary, ignored: Set): { added: Set, removed: Set, updated: Set } { diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index b94999ed40..51ab3a98ac 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -3,9 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; -import { IFileService, FileSystemProviderErrorCode, FileSystemProviderError, IFileContent } from 'vs/platform/files/common/files'; -import { IUserData, UserDataSyncStoreError, UserDataSyncStoreErrorCode, ISynchroniser, SyncStatus, IUserDataSyncStoreService, DEFAULT_IGNORED_SETTINGS, IUserDataSyncLogService, IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IFileService, FileSystemProviderErrorCode, FileSystemProviderError, IFileContent, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; +import { IUserData, UserDataSyncStoreError, UserDataSyncStoreErrorCode, SyncStatus, IUserDataSyncStoreService, DEFAULT_IGNORED_SETTINGS, IUserDataSyncLogService, IUserDataSyncUtilService, IConflictSetting, ISettingsSyncService, CONFIGURATION_SYNC_STORE_KEY, SyncSource } from 'vs/platform/userDataSync/common/userDataSync'; import { VSBuffer } from 'vs/base/common/buffer'; import { parse, ParseError } from 'vs/base/common/json'; import { localize } from 'vs/nls'; @@ -17,18 +16,25 @@ import { joinPath, dirname } from 'vs/base/common/resources'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { startsWith } from 'vs/base/common/strings'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { computeRemoteContent, merge } from 'vs/platform/userDataSync/common/settingsMerge'; +import { updateIgnoredSettings, merge } from 'vs/platform/userDataSync/common/settingsMerge'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; +import * as arrays from 'vs/base/common/arrays'; +import * as objects from 'vs/base/common/objects'; +import { isEmptyObject } from 'vs/base/common/types'; +import { edit } from 'vs/platform/userDataSync/common/content'; +import { AbstractSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer'; interface ISyncPreviewResult { readonly fileContent: IFileContent | null; - readonly remoteUserData: IUserData; + readonly remoteUserData: IUserData | null; readonly hasLocalChanged: boolean; readonly hasRemoteChanged: boolean; - readonly hasConflicts: boolean; + readonly conflicts: IConflictSetting[]; } -export class SettingsSynchroniser extends Disposable implements ISynchroniser { +export class SettingsSynchroniser extends AbstractSynchroniser implements ISettingsSyncService { + + _serviceBrand: any; private static EXTERNAL_USER_DATA_SETTINGS_KEY: string = 'settings'; @@ -39,20 +45,25 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { private _onDidChangStatus: Emitter = this._register(new Emitter()); readonly onDidChangeStatus: Event = this._onDidChangStatus.event; + private _conflicts: IConflictSetting[] = []; + get conflicts(): IConflictSetting[] { return this._conflicts; } + private _onDidChangeConflicts: Emitter = this._register(new Emitter()); + readonly onDidChangeConflicts: Event = this._onDidChangeConflicts.event; + private _onDidChangeLocal: Emitter = this._register(new Emitter()); readonly onDidChangeLocal: Event = this._onDidChangeLocal.event; private readonly lastSyncSettingsResource: URI; constructor( - @IFileService private readonly fileService: IFileService, + @IFileService fileService: IFileService, @IEnvironmentService private readonly environmentService: IEnvironmentService, @IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, @IUserDataSyncUtilService private readonly userDataSyncUtilService: IUserDataSyncUtilService, @IConfigurationService private readonly configurationService: IConfigurationService, ) { - super(); + super(SyncSource.Settings, fileService, environmentService); this.lastSyncSettingsResource = joinPath(this.environmentService.userRoamingDataHome, '.lastSyncSettings.json'); this._register(this.fileService.watch(dirname(this.environmentService.settingsResource))); this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.environmentService.settingsResource))(() => this._onDidChangeLocal.fire())); @@ -63,6 +74,103 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { this._status = status; this._onDidChangStatus.fire(status); } + if (this._status !== SyncStatus.HasConflicts) { + this.setConflicts([]); + } + } + + private setConflicts(conflicts: IConflictSetting[]): void { + if (!arrays.equals(this.conflicts, conflicts, + (a, b) => a.key === b.key && objects.equals(a.localValue, b.localValue) && objects.equals(a.remoteValue, b.remoteValue)) + ) { + this._conflicts = conflicts; + this._onDidChangeConflicts.fire(conflicts); + } + } + + async pull(): Promise { + if (!this.configurationService.getValue('sync.enableSettings')) { + this.logService.info('Settings: Skipped pulling settings as it is disabled.'); + return; + } + + this.stop(); + + try { + this.logService.info('Settings: Started pulling settings...'); + this.setStatus(SyncStatus.Syncing); + + const remoteUserData = await this.getRemoteUserData(); + + if (remoteUserData.content !== null) { + const fileContent = await this.getLocalFileContent(); + const formatUtils = await this.getFormattingOptions(); + // Update ignored settings + const content = updateIgnoredSettings(remoteUserData.content, fileContent ? fileContent.value.toString() : '{}', getIgnoredSettings(this.configurationService), formatUtils); + await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, VSBuffer.fromString(content)); + + this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve({ + conflicts: [], + fileContent, + hasLocalChanged: true, + hasRemoteChanged: false, + remoteUserData + })); + + await this.apply(); + } + + // No remote exists to pull + else { + this.logService.info('Settings: Remote settings does not exist.'); + } + + this.logService.info('Settings: Finished pulling settings.'); + } finally { + this.setStatus(SyncStatus.Idle); + } + } + + async push(): Promise { + if (!this.configurationService.getValue('sync.enableSettings')) { + this.logService.info('Settings: Skipped pushing settings as it is disabled.'); + return; + } + + this.stop(); + + try { + this.logService.info('Settings: Started pushing settings...'); + this.setStatus(SyncStatus.Syncing); + + const fileContent = await this.getLocalFileContent(); + + if (fileContent !== null) { + const formatUtils = await this.getFormattingOptions(); + // Remove ignored settings + const content = updateIgnoredSettings(fileContent.value.toString(), '{}', getIgnoredSettings(this.configurationService), formatUtils); + await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, VSBuffer.fromString(content)); + + this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve({ + conflicts: [], + fileContent, + hasLocalChanged: false, + hasRemoteChanged: true, + remoteUserData: null + })); + + await this.apply(); + } + + // No local exists to push + else { + this.logService.info('Settings: Local settings does not exist.'); + } + + this.logService.info('Settings: Finished pushing settings.'); + } finally { + this.setStatus(SyncStatus.Idle); + } } async sync(_continue?: boolean): Promise { @@ -83,16 +191,77 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { this.logService.trace('Settings: Started synchronizing settings...'); this.setStatus(SyncStatus.Syncing); + return this.doSync([]); + } + stop(): void { + if (this.syncPreviewResultPromise) { + this.syncPreviewResultPromise.cancel(); + this.syncPreviewResultPromise = null; + this.logService.info('Settings: Stopped synchronizing settings.'); + } + this.fileService.del(this.environmentService.settingsSyncPreviewResource); + this.setStatus(SyncStatus.Idle); + } + + async hasPreviouslySynced(): Promise { + const lastSyncData = await this.getLastSyncUserData(); + return !!lastSyncData; + } + + async hasRemoteData(): Promise { + const remoteUserData = await this.getRemoteUserData(); + return remoteUserData.content !== null; + } + + async hasLocalData(): Promise { try { - const result = await this.getPreview(); - if (result.hasConflicts) { + const localFileContent = await this.getLocalFileContent(); + if (localFileContent) { + const formatUtils = await this.getFormattingOptions(); + const content = edit(localFileContent.value.toString(), [CONFIGURATION_SYNC_STORE_KEY], undefined, formatUtils); + const settings = parse(content); + if (!isEmptyObject(settings)) { + return true; + } + } + } catch (error) { + if ((error).fileOperationResult !== FileOperationResult.FILE_NOT_FOUND) { + return true; + } + } + return false; + } + + async resolveConflicts(resolvedConflicts: { key: string, value: any | undefined }[]): Promise { + if (this.status === SyncStatus.HasConflicts) { + this.syncPreviewResultPromise!.cancel(); + this.syncPreviewResultPromise = null; + await this.doSync(resolvedConflicts); + } + } + + async resetLocal(): Promise { + try { + await this.fileService.del(this.lastSyncSettingsResource); + } catch (e) { /* ignore */ } + } + + private async doSync(resolvedConflicts: { key: string, value: any | undefined }[]): Promise { + try { + const result = await this.getPreview(resolvedConflicts); + if (result.conflicts.length) { this.logService.info('Settings: Detected conflicts while synchronizing settings.'); this.setStatus(SyncStatus.HasConflicts); return false; } - await this.apply(); - return true; + try { + await this.apply(); + this.logService.trace('Settings: Finished synchronizing settings.'); + return true; + } finally { + this.setStatus(SyncStatus.Idle); + } } catch (e) { this.syncPreviewResultPromise = null; this.setStatus(SyncStatus.Idle); @@ -110,19 +279,10 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { } } - stop(): void { - if (this.syncPreviewResultPromise) { - this.syncPreviewResultPromise.cancel(); - this.syncPreviewResultPromise = null; - this.logService.info('Settings: Stopped synchronizing settings.'); - } - this.fileService.del(this.environmentService.settingsSyncPreviewResource); - this.setStatus(SyncStatus.Idle); - } - private async continueSync(): Promise { if (this.status === SyncStatus.HasConflicts) { await this.apply(); + this.setStatus(SyncStatus.Idle); } return true; } @@ -151,12 +311,12 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { } if (hasRemoteChanged) { const formatUtils = await this.getFormattingOptions(); - const remoteContent = remoteUserData.content ? computeRemoteContent(content, remoteUserData.content, this.getIgnoredSettings(content), formatUtils) : content; + const remoteContent = remoteUserData?.content ? updateIgnoredSettings(content, remoteUserData.content, getIgnoredSettings(this.configurationService, content), formatUtils) : content; this.logService.info('Settings: Updating remote settings'); - const ref = await this.writeToRemote(remoteContent, remoteUserData.ref); + const ref = await this.writeToRemote(remoteContent, remoteUserData ? remoteUserData.ref : null); remoteUserData = { ref, content }; } - if (remoteUserData.content) { + if (remoteUserData?.content) { this.logService.info('Settings: Updating last synchronised sttings'); await this.updateLastSyncValue(remoteUserData); } @@ -167,9 +327,7 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { this.logService.trace('Settings: No changes found during synchronizing settings.'); } - this.logService.trace('Settings: Finised synchronizing settings.'); this.syncPreviewResultPromise = null; - this.setStatus(SyncStatus.Idle); } private hasErrors(content: string): boolean { @@ -178,43 +336,44 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { return parseErrors.length > 0; } - private getPreview(): Promise { + private getPreview(resolvedConflicts: { key: string, value: any }[]): Promise { if (!this.syncPreviewResultPromise) { - this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(token)); + this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(resolvedConflicts, token)); } return this.syncPreviewResultPromise; } - private async generatePreview(token: CancellationToken): Promise { + private async generatePreview(resolvedConflicts: { key: string, value: any }[], token: CancellationToken): Promise { const lastSyncData = await this.getLastSyncUserData(); - const remoteUserData = await this.userDataSyncStoreService.read(SettingsSynchroniser.EXTERNAL_USER_DATA_SETTINGS_KEY, lastSyncData); + const remoteUserData = await this.getRemoteUserData(lastSyncData); const remoteContent: string | null = remoteUserData.content; // Get file content last to get the latest const fileContent = await this.getLocalFileContent(); let hasLocalChanged: boolean = false; let hasRemoteChanged: boolean = false; - let hasConflicts: boolean = false; + let conflicts: IConflictSetting[] = []; let previewContent = null; if (remoteContent) { const localContent: string = fileContent ? fileContent.value.toString() : '{}'; + + // No action when there are errors if (this.hasErrors(localContent)) { this.logService.error('Settings: Unable to sync settings as there are errors/warning in settings file.'); - return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts }; } - if (!lastSyncData // First time sync + else if (!lastSyncData // First time sync || lastSyncData.content !== localContent // Local has forwarded || lastSyncData.content !== remoteContent // Remote has forwarded ) { this.logService.trace('Settings: Merging remote settings with local settings...'); const formatUtils = await this.getFormattingOptions(); - const result = merge(localContent, remoteContent, lastSyncData ? lastSyncData.content : null, this.getIgnoredSettings(), formatUtils); + const result = merge(localContent, remoteContent, lastSyncData ? lastSyncData.content : null, getIgnoredSettings(this.configurationService), resolvedConflicts, formatUtils); // Sync only if there are changes if (result.hasChanges) { hasLocalChanged = result.mergeContent !== localContent; hasRemoteChanged = result.mergeContent !== remoteContent; - hasConflicts = result.hasConflicts; + conflicts = result.conflicts; previewContent = result.mergeContent; } } @@ -231,7 +390,8 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { await this.fileService.writeFile(this.environmentService.settingsSyncPreviewResource, VSBuffer.fromString(previewContent)); } - return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts }; + this.setConflicts(conflicts); + return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, conflicts }; } private _formattingOptions: Promise | undefined = undefined; @@ -242,29 +402,6 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { return this._formattingOptions; } - private getIgnoredSettings(settingsContent?: string): string[] { - let value: string[] = []; - if (settingsContent) { - const setting = parse(settingsContent); - if (setting) { - value = setting['sync.ignoredSettings']; - } - } else { - value = this.configurationService.getValue('sync.ignoredSettings'); - } - const added: string[] = [], removed: string[] = []; - if (Array.isArray(value)) { - for (const key of value) { - if (startsWith(key, '-')) { - removed.push(key.substring(1)); - } else { - added.push(key); - } - } - } - return [...DEFAULT_IGNORED_SETTINGS, ...added].filter(setting => removed.indexOf(setting) === -1); - } - private async getLastSyncUserData(): Promise { try { const content = await this.fileService.readFile(this.lastSyncSettingsResource); @@ -282,6 +419,10 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { } } + private getRemoteUserData(lastSyncData?: IUserData | null): Promise { + return this.userDataSyncStoreService.read(SettingsSynchroniser.EXTERNAL_USER_DATA_SETTINGS_KEY, lastSyncData || null); + } + private async writeToRemote(content: string, ref: string | null): Promise { return this.userDataSyncStoreService.write(SettingsSynchroniser.EXTERNAL_USER_DATA_SETTINGS_KEY, content, ref); } @@ -289,6 +430,7 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { private async writeToLocal(newContent: string, oldContent: IFileContent | null): Promise { if (oldContent) { // file exists already + await this.backupLocal(oldContent.value); await this.fileService.writeFile(this.environmentService.settingsResource, VSBuffer.fromString(newContent), oldContent); } else { // file does not exist @@ -301,3 +443,26 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { } } + +export function getIgnoredSettings(configurationService: IConfigurationService, settingsContent?: string): string[] { + let value: string[] = []; + if (settingsContent) { + const setting = parse(settingsContent); + if (setting) { + value = setting['sync.ignoredSettings']; + } + } else { + value = configurationService.getValue('sync.ignoredSettings'); + } + const added: string[] = [], removed: string[] = []; + if (Array.isArray(value)) { + for (const key of value) { + if (startsWith(key, '-')) { + removed.push(key.substring(1)); + } else { + added.push(key); + } + } + } + return [...DEFAULT_IGNORED_SETTINGS, ...added].filter(setting => removed.indexOf(setting) === -1); +} diff --git a/src/vs/platform/userDataSync/common/userDataAuthTokenService.ts b/src/vs/platform/userDataSync/common/userDataAuthTokenService.ts new file mode 100644 index 0000000000..c2c5bd0cfa --- /dev/null +++ b/src/vs/platform/userDataSync/common/userDataAuthTokenService.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IUserDataAuthTokenService } from 'vs/platform/userDataSync/common/userDataSync'; + +export class UserDataAuthTokenService extends Disposable implements IUserDataAuthTokenService { + + _serviceBrand: any; + + private _onDidChangeToken: Emitter = this._register(new Emitter()); + readonly onDidChangeToken: Event = this._onDidChangeToken.event; + + private _token: string | undefined; + + constructor() { + super(); + } + + async getToken(): Promise { + return this._token; + } + + async setToken(token: string | undefined): Promise { + if (token !== this._token) { + this._token = token; + this._onDidChangeToken.fire(token); + } + } +} diff --git a/src/vs/platform/userDataSync/common/userDataAutoSync.ts b/src/vs/platform/userDataSync/common/userDataAutoSync.ts index 849f9afe82..488d9a15f5 100644 --- a/src/vs/platform/userDataSync/common/userDataAutoSync.ts +++ b/src/vs/platform/userDataSync/common/userDataAutoSync.ts @@ -3,14 +3,15 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserDataSyncService, SyncStatus, IUserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSync'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { Event } from 'vs/base/common/event'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { timeout } from 'vs/base/common/async'; -import { IAuthTokenService, AuthTokenStatus } from 'vs/platform/auth/common/auth'; +import { Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IUserDataSyncLogService, IUserDataSyncService, SyncStatus, IUserDataAuthTokenService, IUserDataAutoSyncService, IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync'; -export class UserDataAutoSync extends Disposable { +export class UserDataAutoSync extends Disposable implements IUserDataAutoSyncService { + + _serviceBrand: any; private enabled: boolean = false; @@ -18,16 +19,18 @@ export class UserDataAutoSync extends Disposable { @IConfigurationService private readonly configurationService: IConfigurationService, @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, - @IAuthTokenService private readonly authTokenService: IAuthTokenService, + @IUserDataAuthTokenService private readonly userDataAuthTokenService: IUserDataAuthTokenService, + @IUserDataSyncUtilService private readonly userDataSyncUtilService: IUserDataSyncUtilService, ) { super(); - this.updateEnablement(false); - this._register(Event.any(authTokenService.onDidChangeStatus, userDataSyncService.onDidChangeStatus)(() => this.updateEnablement(true))); - this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('sync.enable'))(() => this.updateEnablement(true))); + this.updateEnablement(false, true); + this._register(Event.any(userDataAuthTokenService.onDidChangeToken)(() => this.updateEnablement(true, true))); + this._register(Event.any(userDataSyncService.onDidChangeStatus)(() => this.updateEnablement(true, true))); + this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('sync.enable'))(() => this.updateEnablement(true, false))); } - private updateEnablement(stopIfDisabled: boolean): void { - const enabled = this.isSyncEnabled(); + private async updateEnablement(stopIfDisabled: boolean, auto: boolean): Promise { + const enabled = await this.isSyncEnabled(); if (this.enabled === enabled) { return; } @@ -35,7 +38,7 @@ export class UserDataAutoSync extends Disposable { this.enabled = enabled; if (this.enabled) { this.logService.info('Syncing configuration started'); - this.sync(true); + this.sync(true, auto); return; } else { if (stopIfDisabled) { @@ -46,24 +49,42 @@ export class UserDataAutoSync extends Disposable { } - protected async sync(loop: boolean): Promise { + private async sync(loop: boolean, auto: boolean): Promise { if (this.enabled) { try { + if (auto) { + if (await this.isTurnedOffEverywhere()) { + // Turned off everywhere. Reset & Stop Sync. + await this.userDataSyncService.resetLocal(); + await this.userDataSyncUtilService.updateConfigurationValue('sync.enable', false); + return; + } + } await this.userDataSyncService.sync(); } catch (e) { this.logService.error(e); } if (loop) { await timeout(1000 * 60 * 5); // Loop sync for every 5 min. - this.sync(loop); + this.sync(loop, true); } } } - private isSyncEnabled(): boolean { + private async isTurnedOffEverywhere(): Promise { + const hasRemote = await this.userDataSyncService.hasRemoteData(); + const hasPreviouslySynced = await this.userDataSyncService.hasPreviouslySynced(); + return !hasRemote && hasPreviouslySynced; + } + + private async isSyncEnabled(): Promise { return this.configurationService.getValue('sync.enable') && this.userDataSyncService.status !== SyncStatus.Uninitialized - && this.authTokenService.status === AuthTokenStatus.SignedIn; + && !!(await this.userDataAuthTokenService.getToken()); + } + + triggerAutoSync(): Promise { + return this.sync(false, true); } } diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 6d62118c4b..dc651be558 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -19,7 +19,7 @@ import { IStringDictionary } from 'vs/base/common/collections'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; import { URI } from 'vs/base/common/uri'; -const CONFIGURATION_SYNC_STORE_KEY = 'configurationSync.store'; +export const CONFIGURATION_SYNC_STORE_KEY = 'configurationSync.store'; export const DEFAULT_IGNORED_SETTINGS = [ CONFIGURATION_SYNC_STORE_KEY, @@ -28,6 +28,19 @@ export const DEFAULT_IGNORED_SETTINGS = [ 'sync.enableExtensions', ]; +export interface ISyncConfiguration { + sync: { + enable: boolean, + enableSettings: boolean, + enableKeybindings: boolean, + enableUIState: boolean, + enableExtensions: boolean, + keybindingsPerPlatform: boolean, + ignoredExtensions: string[], + ignoredSettings: string[] + } +} + export function registerConfiguration(): IDisposable { const ignoredSettingsSchemaId = 'vscode://schemas/ignoredSettings'; const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -49,18 +62,24 @@ export function registerConfiguration(): IDisposable { default: true, scope: ConfigurationScope.APPLICATION, }, - 'sync.enableExtensions': { - type: 'boolean', - description: localize('sync.enableExtensions', "Enable synchronizing extensions."), - default: true, - scope: ConfigurationScope.APPLICATION, - }, 'sync.enableKeybindings': { type: 'boolean', description: localize('sync.enableKeybindings', "Enable synchronizing keybindings."), default: true, scope: ConfigurationScope.APPLICATION, }, + 'sync.enableUIState': { + type: 'boolean', + description: localize('sync.enableUIState', "Enable synchronizing UI state (Only Display Language)."), + default: true, + scope: ConfigurationScope.APPLICATION, + }, + 'sync.enableExtensions': { + type: 'boolean', + description: localize('sync.enableExtensions', "Enable synchronizing extensions."), + default: true, + scope: ConfigurationScope.APPLICATION, + }, 'sync.keybindingsPerPlatform': { type: 'boolean', description: localize('sync.keybindingsPerPlatform', "Synchronize keybindings per platform."), @@ -121,22 +140,21 @@ export interface IUserDataSyncStore { url: string; name: string; account: string; + authenticationProviderId: string; } export function getUserDataSyncStore(configurationService: IConfigurationService): IUserDataSyncStore | undefined { const value = configurationService.getValue(CONFIGURATION_SYNC_STORE_KEY); - return value && value.url && value.name && value.account ? value : undefined; + return value && value.url && value.name && value.account && value.authenticationProviderId ? value : undefined; } export const IUserDataSyncStoreService = createDecorator('IUserDataSyncStoreService'); - export interface IUserDataSyncStoreService { _serviceBrand: undefined; - readonly userDataSyncStore: IUserDataSyncStore | undefined; - read(key: string, oldValue: IUserData | null): Promise; write(key: string, content: string, ref: string | null): Promise; + clear(): Promise; } export interface ISyncExtension { @@ -145,10 +163,16 @@ export interface ISyncExtension { enabled: boolean; } +export interface IGlobalState { + argv: IStringDictionary; + storage: IStringDictionary; +} + export const enum SyncSource { - Settings = 1, - Keybindings, - Extensions + Settings = 'Settings', + Keybindings = 'Keybindings', + Extensions = 'Extensions', + UIState = 'UI State' } export const enum SyncStatus { @@ -159,40 +183,70 @@ export const enum SyncStatus { } export interface ISynchroniser { - readonly status: SyncStatus; readonly onDidChangeStatus: Event; readonly onDidChangeLocal: Event; - + pull(): Promise; + push(): Promise; sync(_continue?: boolean): Promise; stop(): void; + hasPreviouslySynced(): Promise + hasRemoteData(): Promise; + hasLocalData(): Promise; + resetLocal(): Promise; } export const IUserDataSyncService = createDecorator('IUserDataSyncService'); - export interface IUserDataSyncService extends ISynchroniser { _serviceBrand: any; readonly conflictsSource: SyncSource | null; - + isFirstTimeSyncAndHasUserData(): Promise; + reset(): Promise; + resetLocal(): Promise; removeExtension(identifier: IExtensionIdentifier): Promise; } +export const IUserDataAutoSyncService = createDecorator('IUserDataAutoSyncService'); +export interface IUserDataAutoSyncService { + _serviceBrand: any; + triggerAutoSync(): Promise; +} + export const IUserDataSyncUtilService = createDecorator('IUserDataSyncUtilService'); - export interface IUserDataSyncUtilService { + _serviceBrand: undefined; + updateConfigurationValue(key: string, value: any): Promise; + resolveUserBindings(userbindings: string[]): Promise>; + resolveFormattingOptions(resource: URI): Promise; + ignoreExtensionsToSync(extensionIdentifiers: IExtensionIdentifier[]): Promise; +} +export const IUserDataAuthTokenService = createDecorator('IUserDataAuthTokenService'); + +export interface IUserDataAuthTokenService { _serviceBrand: undefined; - resolveUserBindings(userbindings: string[]): Promise>; - - resolveFormattingOptions(resource: URI): Promise; + readonly onDidChangeToken: Event; + getToken(): Promise; + setToken(accessToken: string | undefined): Promise; } export const IUserDataSyncLogService = createDecorator('IUserDataSyncLogService'); +export interface IUserDataSyncLogService extends ILogService { } -export interface IUserDataSyncLogService extends ILogService { +export interface IConflictSetting { + key: string; + localValue: any | undefined; + remoteValue: any | undefined; +} +export const ISettingsSyncService = createDecorator('ISettingsSyncService'); +export interface ISettingsSyncService extends ISynchroniser { + _serviceBrand: any; + readonly onDidChangeConflicts: Event; + readonly conflicts: IConflictSetting[]; + resolveConflicts(resolvedConflicts: { key: string, value: any | undefined }[]): Promise; } export const CONTEXT_SYNC_STATE = new RawContextKey('syncStatus', SyncStatus.Uninitialized); diff --git a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts index f800257dab..28ff32a351 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts @@ -5,10 +5,11 @@ import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc'; import { Event } from 'vs/base/common/event'; -import { IUserDataSyncService, IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IUserDataSyncUtilService, ISettingsSyncService, IUserDataAuthTokenService, IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; import { URI } from 'vs/base/common/uri'; import { IStringDictionary } from 'vs/base/common/collections'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; +import type { IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement'; export class UserDataSyncChannel implements IServerChannel { @@ -25,10 +26,84 @@ export class UserDataSyncChannel implements IServerChannel { call(context: any, command: string, args?: any): Promise { switch (command) { case 'sync': return this.service.sync(args[0]); + case 'pull': return this.service.pull(); + case 'push': return this.service.push(); case '_getInitialStatus': return Promise.resolve(this.service.status); case 'getConflictsSource': return Promise.resolve(this.service.conflictsSource); case 'removeExtension': return this.service.removeExtension(args[0]); case 'stop': this.service.stop(); return Promise.resolve(); + case 'reset': return this.service.reset(); + case 'resetLocal': return this.service.resetLocal(); + case 'hasPreviouslySynced': return this.service.hasPreviouslySynced(); + case 'hasRemoteData': return this.service.hasRemoteData(); + case 'hasLocalData': return this.service.hasLocalData(); + case 'isFirstTimeSyncAndHasUserData': return this.service.isFirstTimeSyncAndHasUserData(); + } + throw new Error('Invalid call'); + } +} + +export class SettingsSyncChannel implements IServerChannel { + + constructor(private readonly service: ISettingsSyncService) { } + + listen(_: unknown, event: string): Event { + switch (event) { + case 'onDidChangeStatus': return this.service.onDidChangeStatus; + case 'onDidChangeLocal': return this.service.onDidChangeLocal; + case 'onDidChangeConflicts': return this.service.onDidChangeConflicts; + } + throw new Error(`Event not found: ${event}`); + } + + call(context: any, command: string, args?: any): Promise { + switch (command) { + case 'sync': return this.service.sync(args[0]); + case 'pull': return this.service.pull(); + case 'push': return this.service.push(); + case '_getInitialStatus': return Promise.resolve(this.service.status); + case '_getInitialConflicts': return Promise.resolve(this.service.conflicts); + case 'stop': this.service.stop(); return Promise.resolve(); + case 'resetLocal': return this.service.resetLocal(); + case 'hasPreviouslySynced': return this.service.hasPreviouslySynced(); + case 'hasRemoteData': return this.service.hasRemoteData(); + case 'hasLocalData': return this.service.hasLocalData(); + case 'resolveConflicts': return this.service.resolveConflicts(args[0]); + } + throw new Error('Invalid call'); + } +} + +export class UserDataAutoSyncChannel implements IServerChannel { + + constructor(private readonly service: IUserDataAutoSyncService) { } + + listen(_: unknown, event: string): Event { + throw new Error(`Event not found: ${event}`); + } + + call(context: any, command: string, args?: any): Promise { + switch (command) { + case 'triggerAutoSync': return this.service.triggerAutoSync(); + } + throw new Error('Invalid call'); + } +} + +export class UserDataAuthTokenServiceChannel implements IServerChannel { + constructor(private readonly service: IUserDataAuthTokenService) { } + + listen(_: unknown, event: string): Event { + switch (event) { + case 'onDidChangeToken': return this.service.onDidChangeToken; + } + throw new Error(`Event not found: ${event}`); + } + + call(context: any, command: string, args?: any): Promise { + switch (command) { + case 'setToken': return this.service.setToken(args); + case 'getToken': return this.service.getToken(); } throw new Error('Invalid call'); } @@ -46,6 +121,8 @@ export class UserDataSycnUtilServiceChannel implements IServerChannel { switch (command) { case 'resolveUserKeybindings': return this.service.resolveUserBindings(args[0]); case 'resolveFormattingOptions': return this.service.resolveFormattingOptions(URI.revive(args[0])); + case 'updateConfigurationValue': return this.service.updateConfigurationValue(args[0], args[1]); + case 'ignoreExtensionsToSync': return this.service.ignoreExtensionsToSync(args[0]); } throw new Error('Invalid call'); } @@ -66,5 +143,13 @@ export class UserDataSyncUtilServiceClient implements IUserDataSyncUtilService { return this.channel.call('resolveFormattingOptions', [file]); } + async updateConfigurationValue(key: string, value: any): Promise { + return this.channel.call('updateConfigurationValue', [key, value]); + } + + async ignoreExtensionsToSync(extensionIdentifiers: IExtensionIdentifier[]): Promise { + return this.channel.call('ignoreExtensionsToSync', [extensionIdentifiers]); + } + } diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index e5a16bd79b..4c0b7b2a2f 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -3,15 +3,16 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserDataSyncService, SyncStatus, ISynchroniser, IUserDataSyncStoreService, SyncSource } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, SyncStatus, ISynchroniser, IUserDataSyncStoreService, SyncSource, ISettingsSyncService, IUserDataSyncLogService, IUserDataAuthTokenService } from 'vs/platform/userDataSync/common/userDataSync'; import { Disposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync'; import { Emitter, Event } from 'vs/base/common/event'; import { ExtensionsSynchroniser } from 'vs/platform/userDataSync/common/extensionsSync'; import { IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { IAuthTokenService, AuthTokenStatus } from 'vs/platform/auth/common/auth'; import { KeybindingsSynchroniser } from 'vs/platform/userDataSync/common/keybindingsSync'; +import { GlobalStateSynchroniser } from 'vs/platform/userDataSync/common/globalStateSync'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; export class UserDataSyncService extends Disposable implements IUserDataSyncService { @@ -29,39 +30,78 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ private _conflictsSource: SyncSource | null = null; get conflictsSource(): SyncSource | null { return this._conflictsSource; } - private readonly settingsSynchroniser: SettingsSynchroniser; private readonly keybindingsSynchroniser: KeybindingsSynchroniser; private readonly extensionsSynchroniser: ExtensionsSynchroniser; + private readonly globalStateSynchroniser: GlobalStateSynchroniser; constructor( @IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IAuthTokenService private readonly authTokenService: IAuthTokenService, + @ISettingsSyncService private readonly settingsSynchroniser: ISettingsSyncService, + @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, + @IUserDataAuthTokenService private readonly userDataAuthTokenService: IUserDataAuthTokenService, ) { super(); - this.settingsSynchroniser = this._register(this.instantiationService.createInstance(SettingsSynchroniser)); this.keybindingsSynchroniser = this._register(this.instantiationService.createInstance(KeybindingsSynchroniser)); + this.globalStateSynchroniser = this._register(this.instantiationService.createInstance(GlobalStateSynchroniser)); this.extensionsSynchroniser = this._register(this.instantiationService.createInstance(ExtensionsSynchroniser)); - this.synchronisers = [this.settingsSynchroniser, this.keybindingsSynchroniser, this.extensionsSynchroniser]; + this.synchronisers = [this.settingsSynchroniser, this.keybindingsSynchroniser, this.globalStateSynchroniser, this.extensionsSynchroniser]; this.updateStatus(); if (this.userDataSyncStoreService.userDataSyncStore) { this._register(Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeStatus, () => undefined)))(() => this.updateStatus())); + this._register(this.userDataAuthTokenService.onDidChangeToken(e => this.onDidChangeAuthTokenStatus(e))); } this.onDidChangeLocal = Event.any(...this.synchronisers.map(s => s.onDidChangeLocal)); } + async pull(): Promise { + if (!this.userDataSyncStoreService.userDataSyncStore) { + throw new Error('Not enabled'); + } + if (!(await this.userDataAuthTokenService.getToken())) { + throw new Error('Not Authenticated. Please sign in to start sync.'); + } + for (const synchroniser of this.synchronisers) { + try { + await synchroniser.pull(); + } catch (e) { + this.logService.error(`${this.getSyncSource(synchroniser)}: ${toErrorMessage(e)}`); + } + } + } + + async push(): Promise { + if (!this.userDataSyncStoreService.userDataSyncStore) { + throw new Error('Not enabled'); + } + if (!(await this.userDataAuthTokenService.getToken())) { + throw new Error('Not Authenticated. Please sign in to start sync.'); + } + for (const synchroniser of this.synchronisers) { + try { + await synchroniser.push(); + } catch (e) { + this.logService.error(`${this.getSyncSource(synchroniser)}: ${toErrorMessage(e)}`); + } + } + } + async sync(_continue?: boolean): Promise { if (!this.userDataSyncStoreService.userDataSyncStore) { throw new Error('Not enabled'); } - if (this.authTokenService.status === AuthTokenStatus.SignedOut) { + if (!(await this.userDataAuthTokenService.getToken())) { throw new Error('Not Authenticated. Please sign in to start sync.'); } for (const synchroniser of this.synchronisers) { - if (!await synchroniser.sync(_continue)) { - return false; + try { + if (!await synchroniser.sync(_continue)) { + return false; + } + } catch (e) { + this.logService.error(`${this.getSyncSource(synchroniser)}: ${toErrorMessage(e)}`); } } return true; @@ -76,6 +116,101 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } } + async hasPreviouslySynced(): Promise { + if (!this.userDataSyncStoreService.userDataSyncStore) { + throw new Error('Not enabled'); + } + if (!(await this.userDataAuthTokenService.getToken())) { + throw new Error('Not Authenticated. Please sign in to start sync.'); + } + for (const synchroniser of this.synchronisers) { + if (await synchroniser.hasPreviouslySynced()) { + return true; + } + } + return false; + } + + async hasRemoteData(): Promise { + if (!this.userDataSyncStoreService.userDataSyncStore) { + throw new Error('Not enabled'); + } + if (!(await this.userDataAuthTokenService.getToken())) { + throw new Error('Not Authenticated. Please sign in to start sync.'); + } + for (const synchroniser of this.synchronisers) { + if (await synchroniser.hasRemoteData()) { + return true; + } + } + return false; + } + + async hasLocalData(): Promise { + if (!this.userDataSyncStoreService.userDataSyncStore) { + throw new Error('Not enabled'); + } + if (!(await this.userDataAuthTokenService.getToken())) { + throw new Error('Not Authenticated. Please sign in to start sync.'); + } + for (const synchroniser of this.synchronisers) { + if (await synchroniser.hasLocalData()) { + return true; + } + } + return false; + } + + async isFirstTimeSyncAndHasUserData(): Promise { + if (!this.userDataSyncStoreService.userDataSyncStore) { + throw new Error('Not enabled'); + } + if (!(await this.userDataAuthTokenService.getToken())) { + throw new Error('Not Authenticated. Please sign in to start sync.'); + } + if (await this.hasPreviouslySynced()) { + return false; + } + return await this.hasLocalData(); + } + + async reset(): Promise { + await this.resetRemote(); + await this.resetLocal(); + } + + private async resetRemote(): Promise { + if (!this.userDataSyncStoreService.userDataSyncStore) { + throw new Error('Not enabled'); + } + if (!(await this.userDataAuthTokenService.getToken())) { + throw new Error('Not Authenticated. Please sign in to start sync.'); + } + try { + await this.userDataSyncStoreService.clear(); + this.logService.info('Completed clearing remote data'); + } catch (e) { + this.logService.error(e); + } + } + + async resetLocal(): Promise { + if (!this.userDataSyncStoreService.userDataSyncStore) { + throw new Error('Not enabled'); + } + if (!(await this.userDataAuthTokenService.getToken())) { + throw new Error('Not Authenticated. Please sign in to start sync.'); + } + for (const synchroniser of this.synchronisers) { + try { + await synchroniser.resetLocal(); + } catch (e) { + this.logService.error(`${this.getSyncSource(synchroniser)}: ${toErrorMessage(e)}`); + } + } + this.logService.info('Completed resetting local cache'); + } + removeExtension(identifier: IExtensionIdentifier): Promise { return this.extensionsSynchroniser.removeExtension(identifier); } @@ -106,15 +241,26 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } private computeConflictsSource(): SyncSource | null { - const source = this.synchronisers.filter(s => s.status === SyncStatus.HasConflicts)[0]; - if (source) { - if (source instanceof SettingsSynchroniser) { - return SyncSource.Settings; - } - if (source instanceof KeybindingsSynchroniser) { - return SyncSource.Keybindings; - } + const synchroniser = this.synchronisers.filter(s => s.status === SyncStatus.HasConflicts)[0]; + return synchroniser ? this.getSyncSource(synchroniser) : null; + } + + private getSyncSource(synchroniser: ISynchroniser): SyncSource { + if (synchroniser instanceof SettingsSynchroniser) { + return SyncSource.Settings; + } + if (synchroniser instanceof KeybindingsSynchroniser) { + return SyncSource.Keybindings; + } + if (synchroniser instanceof ExtensionsSynchroniser) { + return SyncSource.Extensions; + } + return SyncSource.UIState; + } + + private onDidChangeAuthTokenStatus(token: string | undefined): void { + if (!token) { + this.stop(); } - return null; } } diff --git a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts index 06dd7512c5..4ab54fb8cf 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts @@ -4,13 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, } from 'vs/base/common/lifecycle'; -import { IUserData, IUserDataSyncStoreService, UserDataSyncStoreErrorCode, UserDataSyncStoreError, IUserDataSyncStore, getUserDataSyncStore } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserData, IUserDataSyncStoreService, UserDataSyncStoreErrorCode, UserDataSyncStoreError, IUserDataSyncStore, getUserDataSyncStore, IUserDataAuthTokenService } from 'vs/platform/userDataSync/common/userDataSync'; import { IRequestService, asText, isSuccess } from 'vs/platform/request/common/request'; import { URI } from 'vs/base/common/uri'; import { joinPath } from 'vs/base/common/resources'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IHeaders, IRequestOptions, IRequestContext } from 'vs/base/parts/request/common/request'; -import { IAuthTokenService } from 'vs/platform/auth/common/auth'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export class UserDataSyncStoreService extends Disposable implements IUserDataSyncStoreService { @@ -22,7 +21,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn constructor( @IConfigurationService configurationService: IConfigurationService, @IRequestService private readonly requestService: IRequestService, - @IAuthTokenService private readonly authTokenService: IAuthTokenService, + @IUserDataAuthTokenService private readonly authTokenService: IUserDataAuthTokenService, ) { super(); this.userDataSyncStore = getUserDataSyncStore(configurationService); @@ -35,6 +34,8 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn const url = joinPath(URI.parse(this.userDataSyncStore.url), 'resource', key, 'latest').toString(); const headers: IHeaders = {}; + // Disable caching as they are cached by synchronisers + headers['Cache-Control'] = 'no-cache'; if (oldValue) { headers['If-None-Match'] = oldValue.ref; } @@ -87,6 +88,21 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn return newRef; } + async clear(): Promise { + if (!this.userDataSyncStore) { + throw new Error('No settings sync store url configured.'); + } + + const url = joinPath(URI.parse(this.userDataSyncStore.url), 'resource').toString(); + const headers: IHeaders = { 'Content-Type': 'text/plain' }; + + const context = await this.request({ type: 'DELETE', url, headers }, CancellationToken.None); + + if (!isSuccess(context)) { + throw new Error('Server returned ' + context.res.statusCode); + } + } + private async request(options: IRequestOptions, token: CancellationToken): Promise { const authToken = await this.authTokenService.getToken(); if (!authToken) { @@ -98,7 +114,6 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn const context = await this.requestService.request(options, token); if (context.res.statusCode === 401) { - this.authTokenService.refreshToken(); // Throw Unauthorized Error throw new UserDataSyncStoreError('Unauthorized', UserDataSyncStoreErrorCode.Unauthroized); } diff --git a/src/vs/platform/userDataSync/electron-browser/userDataAutoSync.ts b/src/vs/platform/userDataSync/electron-browser/userDataAutoSync.ts index e9b360f579..128bb7feff 100644 --- a/src/vs/platform/userDataSync/electron-browser/userDataAutoSync.ts +++ b/src/vs/platform/userDataSync/electron-browser/userDataAutoSync.ts @@ -3,12 +3,11 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserDataSyncService, IUserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IUserDataSyncLogService, IUserDataAuthTokenService, IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync'; import { Event } from 'vs/base/common/event'; import { IElectronService } from 'vs/platform/electron/node/electron'; import { UserDataAutoSync as BaseUserDataAutoSync } from 'vs/platform/userDataSync/common/userDataAutoSync'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IAuthTokenService } from 'vs/platform/auth/common/auth'; export class UserDataAutoSync extends BaseUserDataAutoSync { @@ -17,16 +16,17 @@ export class UserDataAutoSync extends BaseUserDataAutoSync { @IElectronService electronService: IElectronService, @IConfigurationService configurationService: IConfigurationService, @IUserDataSyncLogService logService: IUserDataSyncLogService, - @IAuthTokenService authTokenService: IAuthTokenService, + @IUserDataAuthTokenService authTokenService: IUserDataAuthTokenService, + @IUserDataSyncUtilService userDataSyncUtilService: IUserDataSyncUtilService, ) { - super(configurationService, userDataSyncService, logService, authTokenService); + super(configurationService, userDataSyncService, logService, authTokenService, userDataSyncUtilService); // Sync immediately if there is a local change. this._register(Event.debounce(Event.any( electronService.onWindowFocus, electronService.onWindowOpen, - userDataSyncService.onDidChangeLocal - ), () => undefined, 500)(() => this.sync(false))); + userDataSyncService.onDidChangeLocal, + ), () => undefined, 500)(() => this.triggerAutoSync())); } } diff --git a/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts b/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts index dcff11676a..dcdeb56eac 100644 --- a/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts @@ -9,6 +9,7 @@ import { IStringDictionary } from 'vs/base/common/collections'; import { IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; import { URI } from 'vs/base/common/uri'; +import type { IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement'; suite('KeybindingsMerge - No Conflicts', () => { @@ -731,4 +732,8 @@ class MockUserDataSyncUtilService implements IUserDataSyncUtilService { async resolveFormattingOptions(file?: URI): Promise { return { eol: '\n', insertSpaces: false, tabSize: 4 }; } + + async updateConfigurationValue(key: string, value: any): Promise { } + + async ignoreExtensionsToSync(extensions: IExtensionIdentifier[]): Promise { } } diff --git a/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts b/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts index e2e792b841..82c3ccee8a 100644 --- a/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { merge, computeRemoteContent } from 'vs/platform/userDataSync/common/settingsMerge'; +import { merge, updateIgnoredSettings } from 'vs/platform/userDataSync/common/settingsMerge'; +import { IConflictSetting } from 'vs/platform/userDataSync/common/userDataSync'; const formattingOptions = { eol: '\n', insertSpaces: false, tabSize: 4 }; @@ -13,9 +14,9 @@ suite('SettingsMerge - No Conflicts', () => { test('merge when local and remote are same with one entry', async () => { const localContent = stringify({ 'a': 1 }); const remoteContent = stringify({ 'a': 1 }); - const actual = merge(localContent, remoteContent, null, [], formattingOptions); + const actual = merge(localContent, remoteContent, null, [], [], formattingOptions); assert.ok(!actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, localContent); }); @@ -28,9 +29,9 @@ suite('SettingsMerge - No Conflicts', () => { 'a': 1, 'b': 2 }); - const actual = merge(localContent, remoteContent, null, [], formattingOptions); + const actual = merge(localContent, remoteContent, null, [], [], formattingOptions); assert.ok(!actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, localContent); }); @@ -43,9 +44,9 @@ suite('SettingsMerge - No Conflicts', () => { 'a': 1, 'b': 2 }); - const actual = merge(localContent, remoteContent, null, [], formattingOptions); + const actual = merge(localContent, remoteContent, null, [], [], formattingOptions); assert.ok(!actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, localContent); }); @@ -62,9 +63,9 @@ suite('SettingsMerge - No Conflicts', () => { 'a': 1, 'b': 2 }); - const actual = merge(localContent, remoteContent, baseContent, [], formattingOptions); + const actual = merge(localContent, remoteContent, baseContent, [], [], formattingOptions); assert.ok(!actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, localContent); }); @@ -76,9 +77,9 @@ suite('SettingsMerge - No Conflicts', () => { 'a': 1, 'b': 2 }); - const actual = merge(localContent, remoteContent, null, [], formattingOptions); + const actual = merge(localContent, remoteContent, null, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, remoteContent); }); @@ -96,9 +97,9 @@ suite('SettingsMerge - No Conflicts', () => { 'b': 2, 'c': 3, }); - const actual = merge(localContent, remoteContent, null, [], formattingOptions); + const actual = merge(localContent, remoteContent, null, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, expected); }); @@ -116,9 +117,9 @@ suite('SettingsMerge - No Conflicts', () => { 'b': 2, 'c': 3, }); - const actual = merge(localContent, remoteContent, localContent, [], formattingOptions); + const actual = merge(localContent, remoteContent, localContent, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, expected); }); @@ -130,9 +131,9 @@ suite('SettingsMerge - No Conflicts', () => { const remoteContent = stringify({ 'a': 1, }); - const actual = merge(localContent, remoteContent, localContent, [], formattingOptions); + const actual = merge(localContent, remoteContent, localContent, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, remoteContent); }); @@ -141,9 +142,9 @@ suite('SettingsMerge - No Conflicts', () => { 'a': 1, }); const remoteContent = stringify({}); - const actual = merge(localContent, remoteContent, localContent, [], formattingOptions); + const actual = merge(localContent, remoteContent, localContent, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.deepEqual(JSON.parse(actual.mergeContent), {}); }); @@ -154,9 +155,9 @@ suite('SettingsMerge - No Conflicts', () => { const remoteContent = stringify({ 'a': 2 }); - const actual = merge(localContent, remoteContent, localContent, [], formattingOptions); + const actual = merge(localContent, remoteContent, localContent, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, remoteContent); }); @@ -170,9 +171,9 @@ suite('SettingsMerge - No Conflicts', () => { 'c': 3, 'd': 4, }); - const actual = merge(localContent, remoteContent, localContent, [], formattingOptions); + const actual = merge(localContent, remoteContent, localContent, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, remoteContent); }); @@ -186,9 +187,9 @@ suite('SettingsMerge - No Conflicts', () => { const remoteContent = stringify({ 'a': 1, }); - const actual = merge(localContent, remoteContent, null, [], formattingOptions); + const actual = merge(localContent, remoteContent, null, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, localContent); }); @@ -202,9 +203,9 @@ suite('SettingsMerge - No Conflicts', () => { const remoteContent = stringify({ 'a': 1, }); - const actual = merge(localContent, remoteContent, remoteContent, [], formattingOptions); + const actual = merge(localContent, remoteContent, remoteContent, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, localContent); }); @@ -219,9 +220,9 @@ suite('SettingsMerge - No Conflicts', () => { 'c': 3, 'd': 4, }); - const actual = merge(localContent, remoteContent, remoteContent, [], formattingOptions); + const actual = merge(localContent, remoteContent, remoteContent, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, localContent); }); @@ -234,9 +235,9 @@ suite('SettingsMerge - No Conflicts', () => { 'a': 2, 'c': 2, }); - const actual = merge(localContent, remoteContent, remoteContent, [], formattingOptions); + const actual = merge(localContent, remoteContent, remoteContent, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, localContent); }); @@ -250,9 +251,9 @@ suite('SettingsMerge - No Conflicts', () => { const remoteContent = stringify({ 'a': 1, }); - const actual = merge(localContent, remoteContent, remoteContent, [], formattingOptions); + const actual = merge(localContent, remoteContent, remoteContent, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, localContent); }); @@ -267,9 +268,10 @@ suite('SettingsMerge - Conflicts', () => { const remoteContent = stringify({ 'a': 2 }); - const actual = merge(localContent, remoteContent, null, [], formattingOptions); + const expectedConflicts: IConflictSetting[] = [{ key: 'a', localValue: 1, remoteValue: 2 }]; + const actual = merge(localContent, remoteContent, null, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(actual.hasConflicts); + assert.deepEqual(actual.conflicts, expectedConflicts); assert.equal(actual.mergeContent, `{ <<<<<<< local @@ -290,9 +292,10 @@ suite('SettingsMerge - Conflicts', () => { const remoteContent = stringify({ 'b': 2 }); - const actual = merge(localContent, remoteContent, baseContent, [], formattingOptions); + const expectedConflicts: IConflictSetting[] = [{ key: 'a', localValue: 2, remoteValue: undefined }]; + const actual = merge(localContent, remoteContent, baseContent, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(actual.hasConflicts); + assert.deepEqual(actual.conflicts, expectedConflicts); assert.equal(actual.mergeContent, `{ <<<<<<< local @@ -311,9 +314,10 @@ suite('SettingsMerge - Conflicts', () => { const remoteContent = stringify({ 'a': 2 }); - const actual = merge(localContent, remoteContent, baseContent, [], formattingOptions); + const expectedConflicts: IConflictSetting[] = [{ key: 'a', localValue: undefined, remoteValue: 2 }]; + const actual = merge(localContent, remoteContent, baseContent, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(actual.hasConflicts); + assert.deepEqual(actual.conflicts, expectedConflicts); assert.equal(actual.mergeContent, `{ <<<<<<< local @@ -343,9 +347,15 @@ suite('SettingsMerge - Conflicts', () => { 'd': 6, 'e': 5, }); - const actual = merge(localContent, remoteContent, baseContent, [], formattingOptions); + const expectedConflicts: IConflictSetting[] = [ + { key: 'b', localValue: undefined, remoteValue: 3 }, + { key: 'a', localValue: 2, remoteValue: undefined }, + { key: 'e', localValue: 4, remoteValue: 5 }, + { key: 'd', localValue: 5, remoteValue: 6 }, + ]; + const actual = merge(localContent, remoteContent, baseContent, [], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(actual.hasConflicts); + assert.deepEqual(actual.conflicts, expectedConflicts); assert.equal(actual.mergeContent, `{ <<<<<<< local @@ -371,6 +381,46 @@ suite('SettingsMerge - Conflicts', () => { }`); }); + test('resolve when local and remote has moved forwareded with conflicts', async () => { + const baseContent = stringify({ + 'a': 1, + 'b': 2, + 'c': 3, + 'd': 4, + }); + const localContent = stringify({ + 'a': 2, + 'c': 3, + 'd': 5, + 'e': 4, + 'f': 1, + }); + const remoteContent = stringify({ + 'b': 3, + 'c': 3, + 'd': 6, + 'e': 5, + }); + const expectedConflicts: IConflictSetting[] = [ + { key: 'd', localValue: 5, remoteValue: 6 }, + ]; + const actual = merge(localContent, remoteContent, baseContent, [], [{ key: 'a', value: 2 }, { key: 'b', value: undefined }, { key: 'e', value: 5 }], formattingOptions); + assert.ok(actual.hasChanges); + assert.deepEqual(actual.conflicts, expectedConflicts); + assert.equal(actual.mergeContent, + `{ + "a": 2, + "c": 3, +<<<<<<< local + "d": 5, +======= + "d": 6, +>>>>>>> remote + "e": 5, + "f": 1 +}`); + }); + }); suite('SettingsMerge - Ignored Settings', () => { @@ -378,9 +428,9 @@ suite('SettingsMerge - Ignored Settings', () => { test('ignored setting is not merged when changed in local and remote', async () => { const localContent = stringify({ 'a': 1 }); const remoteContent = stringify({ 'a': 2 }); - const actual = merge(localContent, remoteContent, null, ['a'], formattingOptions); + const actual = merge(localContent, remoteContent, null, ['a'], [], formattingOptions); assert.ok(!actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, localContent); }); @@ -388,45 +438,45 @@ suite('SettingsMerge - Ignored Settings', () => { const baseContent = stringify({ 'a': 0 }); const localContent = stringify({ 'a': 1 }); const remoteContent = stringify({ 'a': 2 }); - const actual = merge(localContent, remoteContent, baseContent, ['a'], formattingOptions); + const actual = merge(localContent, remoteContent, baseContent, ['a'], [], formattingOptions); assert.ok(!actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, localContent); }); test('ignored setting is not merged when added in remote', async () => { const localContent = stringify({}); const remoteContent = stringify({ 'a': 1 }); - const actual = merge(localContent, remoteContent, null, ['a'], formattingOptions); + const actual = merge(localContent, remoteContent, null, ['a'], [], formattingOptions); assert.ok(!actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, localContent); }); test('ignored setting is not merged when added in remote from base', async () => { const localContent = stringify({ 'b': 2 }); const remoteContent = stringify({ 'a': 1, 'b': 2 }); - const actual = merge(localContent, remoteContent, localContent, ['a'], formattingOptions); + const actual = merge(localContent, remoteContent, localContent, ['a'], [], formattingOptions); assert.ok(!actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, localContent); }); test('ignored setting is not merged when removed in remote', async () => { const localContent = stringify({ 'a': 1 }); const remoteContent = stringify({}); - const actual = merge(localContent, remoteContent, null, ['a'], formattingOptions); + const actual = merge(localContent, remoteContent, null, ['a'], [], formattingOptions); assert.ok(!actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, localContent); }); test('ignored setting is not merged when removed in remote from base', async () => { const localContent = stringify({ 'a': 2 }); const remoteContent = stringify({}); - const actual = merge(localContent, remoteContent, localContent, ['a'], formattingOptions); + const actual = merge(localContent, remoteContent, localContent, ['a'], [], formattingOptions); assert.ok(!actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, localContent); }); @@ -453,9 +503,9 @@ suite('SettingsMerge - Ignored Settings', () => { 'a': 1, 'b': 3, }); - const actual = merge(localContent, remoteContent, baseContent, ['a', 'e'], formattingOptions); + const actual = merge(localContent, remoteContent, baseContent, ['a', 'e'], [], formattingOptions); assert.ok(actual.hasChanges); - assert.ok(!actual.hasConflicts); + assert.equal(actual.conflicts.length, 0); assert.equal(actual.mergeContent, expectedContent); }); @@ -478,12 +528,14 @@ suite('SettingsMerge - Ignored Settings', () => { 'b': 3, 'e': 6, }); - const actual = merge(localContent, remoteContent, baseContent, ['a', 'e'], formattingOptions); - //'{\n\t"a": 1,\n\n<<<<<<< local\t"b": 4,\n=======\n\t"b": 3,\n>>>>>>> remote' - //'{\n\t"a": 1,\n<<<<<<< local\n\t"b": 4,\n=======\n\t"b": 3,\n>>>>>>> remote\n<<<<<<< local\n\t"d": 5\n=======\n>>>>>>> remote\n}' + const expectedConflicts: IConflictSetting[] = [ + { key: 'd', localValue: 5, remoteValue: undefined }, + { key: 'b', localValue: 4, remoteValue: 3 }, + ]; + const actual = merge(localContent, remoteContent, baseContent, ['a', 'e'], [], formattingOptions); assert.ok(actual.hasChanges); assert.ok(actual.hasChanges); - assert.ok(actual.hasConflicts); + assert.deepEqual(actual.conflicts, expectedConflicts); assert.equal(actual.mergeContent, `{ "a": 1, @@ -515,7 +567,7 @@ suite('SettingsMerge - Compute Remote Content', () => { 'd': 4, 'e': 6, }); - const actual = computeRemoteContent(localContent, remoteContent, [], formattingOptions); + const actual = updateIgnoredSettings(localContent, remoteContent, [], formattingOptions); assert.equal(actual, localContent); }); @@ -536,7 +588,7 @@ suite('SettingsMerge - Compute Remote Content', () => { 'b': 2, 'c': 3, }); - const actual = computeRemoteContent(localContent, remoteContent, ['a'], formattingOptions); + const actual = updateIgnoredSettings(localContent, remoteContent, ['a'], formattingOptions); assert.equal(actual, expected); }); diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 6c76d3998d..e191b9fc1b 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -13,7 +13,7 @@ import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup'; import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment'; import { IStateService } from 'vs/platform/state/node/state'; import { CodeWindow, defaultWindowState } from 'vs/code/electron-main/window'; -import { ipcMain as ipc, screen, BrowserWindow, systemPreferences, MessageBoxOptions, Display, app } from 'electron'; +import { ipcMain as ipc, screen, BrowserWindow, MessageBoxOptions, Display, app, nativeTheme } from 'electron'; import { parseLineAndColumnAware } from 'vs/code/node/paths'; import { ILifecycleMainService, UnloadReason, LifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -226,16 +226,13 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // React to HC color scheme changes (Windows) if (isWindows) { - const onHighContrastChange = () => { - if (systemPreferences.isInvertedColorScheme() || systemPreferences.isHighContrastColorScheme()) { + nativeTheme.on('updated', () => { + if (nativeTheme.shouldUseInvertedColorScheme || nativeTheme.shouldUseHighContrastColors) { this.sendToAll('vscode:enterHighContrast'); } else { this.sendToAll('vscode:leaveHighContrast'); } - }; - - systemPreferences.on('inverted-color-scheme-changed', () => onHighContrastChange()); - systemPreferences.on('high-contrast-color-scheme-changed', () => onHighContrastChange()); + }); } // When a window looses focus, save all windows state. This allows to diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts index f9eaf0c21d..2ce78885b8 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts @@ -320,7 +320,9 @@ suite('WorkspacesMainService', () => { service.deleteUntitledWorkspaceSync(workspace); }); - test('getUntitledWorkspaceSync', async () => { + test('getUntitledWorkspaceSync', async function () { + this.retries(3); + let untitled = service.getUntitledWorkspacesSync(); assert.equal(untitled.length, 0); diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index e9b1876d71..d8ee83f620 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -1193,7 +1193,6 @@ declare module 'vscode' { * A complex edit that will be applied in one transaction on a TextEditor. * This holds a description of the edits and if the edits are valid (i.e. no overlapping regions, document was not changed in the meantime, etc.) * they can be applied on a [document](#TextDocument) associated with a [text editor](#TextEditor). - * */ export interface TextEditorEdit { /** @@ -1584,12 +1583,12 @@ declare module 'vscode' { label: string; /** - * A human-readable string which is rendered less prominent. + * A human-readable string which is rendered less prominent in the same line. */ description?: string; /** - * A human-readable string which is rendered less prominent. + * A human-readable string which is rendered less prominent in a separate line. */ detail?: string; @@ -1622,7 +1621,7 @@ declare module 'vscode' { matchOnDetail?: boolean; /** - * An optional string to show as place holder in the input box to guide the user what to pick on. + * An optional string to show as placeholder in the input box to guide the user what to pick on. */ placeHolder?: string; @@ -1648,7 +1647,7 @@ declare module 'vscode' { export interface WorkspaceFolderPickOptions { /** - * An optional string to show as place holder in the input box to guide the user what to pick on. + * An optional string to show as placeholder in the input box to guide the user what to pick on. */ placeHolder?: string; @@ -1796,7 +1795,7 @@ declare module 'vscode' { prompt?: string; /** - * An optional string to show as place holder in the input box to guide the user what to type. + * An optional string to show as placeholder in the input box to guide the user what to type. */ placeHolder?: string; @@ -2318,7 +2317,7 @@ declare module 'vscode' { /** * The declaration of a symbol representation as one or many [locations](#Location) - * or [location links][#LocationLink]. + * or [location links](#LocationLink). */ export type Declaration = Location | Location[] | LocationLink[]; @@ -3414,15 +3413,17 @@ declare module 'vscode' { insertText?: string | SnippetString; /** - * A range of text that should be replaced by this completion item. + * A range or a insert and replace range selecting the text that should be replaced by this completion item. * - * Defaults to a range from the start of the [current word](#TextDocument.getWordRangeAtPosition) to the - * current position. + * When omitted, the range of the [current word](#TextDocument.getWordRangeAtPosition) is used as replace-range + * and as insert-range the start of the [current word](#TextDocument.getWordRangeAtPosition) to the + * current position is used. * - * *Note:* The range must be a [single line](#Range.isSingleLine) and it must + * *Note 1:* A range must be a [single line](#Range.isSingleLine) and it must * [contain](#Range.contains) the position at which completion has been [requested](#CompletionItemProvider.provideCompletionItems). + * *Note 2:* A insert range must be a prefix of a replace range, that means it must be contained and starting at the same position. */ - range?: Range; + range?: Range | { inserting: Range; replacing: Range; }; /** * An optional set of characters that when pressed while this completion is active will accept it first and @@ -4298,7 +4299,7 @@ declare module 'vscode' { * * The *effective* value (returned by [`get`](#WorkspaceConfiguration.get)) * is computed like this: `defaultValue` overridden by `globalValue`, - * `globalValue` overridden by `workspaceValue`. `workspaceValue` overwridden by `workspaceFolderValue`. + * `globalValue` overridden by `workspaceValue`. `workspaceValue` overridden by `workspaceFolderValue`. * Refer to [Settings](https://code.visualstudio.com/docs/getstarted/settings) * for more information. * @@ -4854,6 +4855,13 @@ declare module 'vscode' { */ readonly processId: Thenable; + /** + * The object used to initialize the terminal, this is useful for example to detecting the + * shell type of when the terminal was not launched by this extension or for detecting what + * folder the shell was launched in. + */ + readonly creationOptions: Readonly; + /** * Send text to the terminal. The text is written to the stdin of the underlying pty process * (shell) of the terminal. @@ -9179,7 +9187,7 @@ declare module 'vscode' { value: string; /** - * A string to show as place holder in the input box to guide the user. + * A string to show as placeholder in the input box to guide the user. */ placeholder: string; } @@ -9511,6 +9519,21 @@ declare module 'vscode' { * @return The resolved debug configuration or undefined or null. */ resolveDebugConfiguration?(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): ProviderResult; + + /** + * This hook is directly called after 'resolveDebugConfiguration' but with all variables substituted. + * It can be used to resolve or verify a [debug configuration](#DebugConfiguration) by filling in missing values or by adding/changing/removing attributes. + * If more than one debug configuration provider is registered for the same type, the 'resolveDebugConfigurationWithSubstitutedVariables' calls are chained + * in arbitrary order and the initial debug configuration is piped through the chain. + * Returning the value 'undefined' prevents the debug session from starting. + * Returning the value 'null' prevents the debug session from starting and opens the underlying debug configuration instead. + * + * @param folder The workspace folder from which the configuration originates from or `undefined` for a folderless setup. + * @param debugConfiguration The [debug configuration](#DebugConfiguration) to resolve. + * @param token A cancellation token. + * @return The resolved debug configuration or undefined or null. + */ + resolveDebugConfigurationWithSubstitutedVariables?(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): ProviderResult; } /** diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index adac6a2ebf..14c3e149a9 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -16,7 +16,52 @@ declare module 'vscode' { - //#region Alex - resolvers, AlexR - ports + // #region auth provider: https://github.com/microsoft/vscode/issues/88309 + + export interface Session { + id: string; + accessToken: string; + displayName: string; + } + + export interface AuthenticationProvider { + readonly id: string; + readonly displayName: string; + readonly onDidChangeSessions: Event; + + /** + * Returns an array of current sessions. + */ + getSessions(): Promise>; + + /** + * Prompts a user to login. + */ + login(): Promise; + logout(sessionId: string): Promise; + } + + export namespace authentication { + export function registerAuthenticationProvider(provider: AuthenticationProvider): Disposable; + + /** + * Fires with the provider id that was registered or unregistered. + */ + export const onDidRegisterAuthenticationProvider: Event; + export const onDidUnregisterAuthenticationProvider: Event; + + /** + * Fires with the provider id that changed sessions. + */ + export const onDidChangeSessions: Event; + export function login(providerId: string): Promise; + export function logout(providerId: string, accountId: string): Promise; + export function getSessions(providerId: string): Promise>; + } + + //#endregion + + //#region Alex - resolvers export interface RemoteAuthorityResolverContext { resolveAttempt: number; @@ -40,10 +85,13 @@ declare module 'vscode' { label?: string; } - export interface Tunnel { + export interface TunnelDescription { remoteAddress: { port: number, host: string }; //The complete local address(ex. localhost:1234) localAddress: string; + } + + export interface Tunnel extends TunnelDescription { // Implementers of Tunnel should fire onDidDispose when dispose is called. onDidDispose: Event; dispose(): void; @@ -58,7 +106,9 @@ declare module 'vscode' { * The localAddress should be the complete local address (ex. localhost:1234) for connecting to the port. Tunnels provided through * detected are read-only from the forwarded ports UI. */ - environmentTunnels?: { remoteAddress: { port: number, host: string }, localAddress: string }[]; + environmentTunnels?: TunnelDescription[]; + + hideCandidatePorts?: boolean; } export type ResolverResult = ResolvedAuthority & ResolvedOptions & TunnelInformation; @@ -130,8 +180,7 @@ declare module 'vscode' { /** * The result id of the tokens. * - * On a next call to `provideSemanticTokens`, if VS Code still holds in memory this result, - * the result id will be passed in as `SemanticTokensRequestOptions.previousResultId`. + * This is the id that will be passed to `DocumentSemanticTokensProvider.provideDocumentSemanticTokensEdits` (if implemented). */ readonly resultId?: string; readonly data: Uint32Array; @@ -143,8 +192,7 @@ declare module 'vscode' { /** * The result id of the tokens. * - * On a next call to `provideSemanticTokens`, if VS Code still holds in memory this result, - * the result id will be passed in as `SemanticTokensRequestOptions.previousResultId`. + * This is the id that will be passed to `DocumentSemanticTokensProvider.provideDocumentSemanticTokensEdits` (if implemented). */ readonly resultId?: string; readonly edits: SemanticTokensEdit[]; @@ -160,21 +208,11 @@ declare module 'vscode' { constructor(start: number, deleteCount: number, data?: Uint32Array); } - export interface SemanticTokensRequestOptions { - readonly ranges?: readonly Range[]; - /** - * The previous result id that the editor still holds in memory. - * - * Only when this is set it is safe for a `SemanticTokensProvider` to return `SemanticTokensEdits`. - */ - readonly previousResultId?: string; - } - /** - * The semantic tokens provider interface defines the contract between extensions and + * The document semantic tokens provider interface defines the contract between extensions and * semantic tokens. */ - export interface SemanticTokensProvider { + export interface DocumentSemanticTokensProvider { /** * A file can contain many tokens, perhaps even hundreds of thousands of tokens. Therefore, to improve * the memory consumption around describing semantic tokens, we have decided to avoid allocating an object @@ -182,21 +220,18 @@ declare module 'vscode' { * of each token is expressed relative to the token before it because most tokens remain stable relative to * each other when edits are made in a file. * - * * --- - * In short, each token takes 5 integers to represent, so a specific token `i` in the file consists of the following fields: + * In short, each token takes 5 integers to represent, so a specific token `i` in the file consists of the following array indices: * - at index `5*i` - `deltaLine`: token line number, relative to the previous token * - at index `5*i+1` - `deltaStart`: token start character, relative to the previous token (relative to 0 or the previous token's start if they are on the same line) * - at index `5*i+2` - `length`: the length of the token. A token cannot be multiline. * - at index `5*i+3` - `tokenType`: will be looked up in `SemanticTokensLegend.tokenTypes` * - at index `5*i+4` - `tokenModifiers`: each set bit will be looked up in `SemanticTokensLegend.tokenModifiers` * - * - * * --- * ### How to encode tokens * - * Here is an example for encoding a file with 3 tokens: + * Here is an example for encoding a file with 3 tokens in a uint32 array: * ``` * { line: 2, startChar: 5, length: 3, tokenType: "properties", tokenModifiers: ["private", "static"] }, * { line: 2, startChar: 10, length: 4, tokenType: "types", tokenModifiers: [] }, @@ -235,8 +270,12 @@ declare module 'vscode' { * // 1st token, 2nd token, 3rd token * [ 2,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] * ``` - * - * + */ + provideDocumentSemanticTokens(document: TextDocument, token: CancellationToken): ProviderResult; + + /** + * Instead of always returning all the tokens in a file, it is possible for a `DocumentSemanticTokensProvider` to implement + * this method (`updateSemanticTokens`) and then return incremental updates to the previously provided semantic tokens. * * --- * ### How tokens change when the document changes @@ -257,8 +296,8 @@ declare module 'vscode' { * ``` * It is possible to express these new tokens in terms of an edit applied to the previous tokens: * ``` - * [ 2,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] - * [ 3,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] + * [ 2,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] // old tokens + * [ 3,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] // new tokens * * edit: { start: 0, deleteCount: 1, data: [3] } // replace integer at offset 0 with 3 * ``` @@ -277,51 +316,56 @@ declare module 'vscode' { * ``` * Again, it is possible to express these new tokens in terms of an edit applied to the previous tokens: * ``` - * [ 3,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] - * [ 3,5,3,0,3, 0,5,4,1,0, 1,3,5,0,2, 2,2,7,2,0, ] + * [ 3,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] // old tokens + * [ 3,5,3,0,3, 0,5,4,1,0, 1,3,5,0,2, 2,2,7,2,0, ] // new tokens * * edit: { start: 10, deleteCount: 1, data: [1,3,5,0,2,2] } // replace integer at offset 10 with [1,3,5,0,2,2] * ``` * - * - * - * --- - * ### When to return `SemanticTokensEdits` - * - * When doing edits, it is possible that multiple edits occur until VS Code decides to invoke the semantic tokens provider. - * In principle, each call to `provideSemanticTokens` can return a full representations of the semantic tokens, and that would - * be a perfectly reasonable semantic tokens provider implementation. - * - * However, when having a language server running in a separate process, transferring all the tokens between processes - * might be slow, so VS Code allows to return the new tokens expressed in terms of multiple edits applied to the previous - * tokens. - * - * To clearly define what "previous tokens" means, it is possible to return a `resultId` with the semantic tokens. If the - * editor still has in memory the previous result, the editor will pass in options the previous `resultId` at - * `SemanticTokensRequestOptions.previousResultId`. Only when the editor passes in the previous `resultId`, it is allowed - * that a semantic tokens provider returns the new tokens expressed as edits to be applied to the previous result. Even in this - * case, the semantic tokens provider needs to return a new `resultId` that will identify these new tokens as a basis - * for the next request. - * - * *NOTE 1*: It is illegal to return `SemanticTokensEdits` if `options.previousResultId` is not set. - * *NOTE 2*: All edits in `SemanticTokensEdits` contain indices in the old integers array, so they all refer to the previous result state. + * *NOTE*: When doing edits, it is possible that multiple edits occur until VS Code decides to invoke the semantic tokens provider. + * *NOTE*: If the provider cannot compute `SemanticTokensEdits`, it can "give up" and return all the tokens in the document again. + * *NOTE*: All edits in `SemanticTokensEdits` contain indices in the old integers array, so they all refer to the previous result state. */ - provideSemanticTokens(document: TextDocument, options: SemanticTokensRequestOptions, token: CancellationToken): ProviderResult; + provideDocumentSemanticTokensEdits?(document: TextDocument, previousResultId: string, token: CancellationToken): ProviderResult; + } + + /** + * The document range semantic tokens provider interface defines the contract between extensions and + * semantic tokens. + */ + export interface DocumentRangeSemanticTokensProvider { + /** + * See [provideDocumentSemanticTokens](#DocumentSemanticTokensProvider.provideDocumentSemanticTokens). + */ + provideDocumentRangeSemanticTokens(document: TextDocument, range: Range, token: CancellationToken): ProviderResult; } export namespace languages { /** - * Register a semantic tokens provider. + * Register a semantic tokens provider for a whole document. * * Multiple providers can be registered for a language. In that case providers are sorted * by their [score](#languages.match) and the best-matching provider is used. Failure * of the selected provider will cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. - * @param provider A semantic tokens provider. + * @param provider A document semantic tokens provider. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ - export function registerSemanticTokensProvider(selector: DocumentSelector, provider: SemanticTokensProvider, legend: SemanticTokensLegend): Disposable; + export function registerDocumentSemanticTokensProvider(selector: DocumentSelector, provider: DocumentSemanticTokensProvider, legend: SemanticTokensLegend): Disposable; + + /** + * Register a semantic tokens provider for a document range. + * + * Multiple providers can be registered for a language. In that case providers are sorted + * by their [score](#languages.match) and the best-matching provider is used. Failure + * of the selected provider will cause a failure of the whole operation. + * + * @param selector A selector that defines the documents this provider is applicable to. + * @param provider A document range semantic tokens provider. + * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + */ + export function registerDocumentRangeSemanticTokensProvider(selector: DocumentSelector, provider: DocumentRangeSemanticTokensProvider, legend: SemanticTokensLegend): Disposable; } //#endregion @@ -788,7 +832,7 @@ declare module 'vscode' { //#endregion - //#region Debug + //#region Debug: // deprecated @@ -944,7 +988,7 @@ declare module 'vscode' { * The exit code that a terminal exited with, it can have the following values: * - Zero: the terminal process or custom execution succeeded. * - Non-zero: the terminal process or custom execution failed. - * - `undefined`: the user forcefully closed the terminal or a custom execution exited + * - `undefined`: the user forcibly closed the terminal or a custom execution exited * without providing an exit code. */ readonly code: number | undefined; @@ -969,19 +1013,6 @@ declare module 'vscode' { //#endregion - //#region Terminal creation options https://github.com/microsoft/vscode/issues/63052 - - export interface Terminal { - /** - * The object used to initialize the terminal, this is useful for things like detecting the - * shell type of shells not launched by the extension or detecting what folder the shell was - * launched in. - */ - readonly creationOptions: Readonly; - } - - //#endregion - //#region Terminal dimensions property and change event https://github.com/microsoft/vscode/issues/55718 /** @@ -1248,25 +1279,6 @@ declare module 'vscode' { //#endregion - //#region insert/replace completions: https://github.com/microsoft/vscode/issues/10266 - - export interface CompletionItem { - - /** - * A range or a insert and replace range selecting the text that should be replaced by this completion item. - * - * When omitted, the range of the [current word](#TextDocument.getWordRangeAtPosition) is used as replace-range - * and as insert-range the start of the [current word](#TextDocument.getWordRangeAtPosition) to the - * current position is used. - * - * *Note 1:* A range must be a [single line](#Range.isSingleLine) and it must - * [contain](#Range.contains) the position at which completion has been [requested](#CompletionItemProvider.provideCompletionItems). - * *Note 2:* A insert range must be a prefix of a replace range, that means it must be contained and starting at the same position. - */ - range2?: Range | { inserting: Range; replacing: Range; }; - } - - //#endregion //#region allow QuickPicks to skip sorting: https://github.com/microsoft/vscode/issues/73904 @@ -1313,7 +1325,7 @@ declare module 'vscode' { //#region Language specific settings: https://github.com/microsoft/vscode/issues/26707 - export type ConfigurationScope = Uri | TextDocument | WorkspaceFolder | { resource: Uri, languageId: string }; + export type ConfigurationScope = Uri | TextDocument | WorkspaceFolder | { uri?: Uri, languageId: string }; /** * An event describing the change in Configuration @@ -1439,6 +1451,8 @@ declare module 'vscode' { workspaceLanguageValue?: T; workspaceFolderLanguageValue?: T; + languages?: string[]; + } | undefined; /** @@ -1510,4 +1524,15 @@ declare module 'vscode' { export const onDidChangeActiveColorTheme: Event; } + //#endregion + + + //#region https://github.com/microsoft/vscode/issues/39441 + + export interface CompletionList { + isDetailsResolved?: boolean; + } + + //#endregion + } diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index 97b04a9b5a..ded24e7b19 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -59,6 +59,7 @@ import './mainThreadComments'; // import './mainThreadTask'; {{SQL CARBON EDIT}} @anthonydresser comment out task import './mainThreadLabelService'; import './mainThreadTunnelService'; +import './mainThreadAuthentication'; import 'vs/workbench/api/common/apiCommands'; export class ExtensionPoints implements IWorkbenchContribution { diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts new file mode 100644 index 0000000000..201a692e93 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import * as modes from 'vs/editor/common/modes'; +import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; +import { ExtHostAuthenticationShape, ExtHostContext, IExtHostContext, MainContext, MainThreadAuthenticationShape } from '../common/extHost.protocol'; + +export class MainThreadAuthenticationProvider { + constructor( + private readonly _proxy: ExtHostAuthenticationShape, + public readonly id: string + ) { } + + getSessions(): Promise> { + return this._proxy.$getSessions(this.id); + } + + login(): Promise { + return this._proxy.$login(this.id); + } + + logout(accountId: string): Promise { + return this._proxy.$logout(this.id, accountId); + } +} + +@extHostNamedCustomer(MainContext.MainThreadAuthentication) +export class MainThreadAuthentication extends Disposable implements MainThreadAuthenticationShape { + private readonly _proxy: ExtHostAuthenticationShape; + + constructor( + extHostContext: IExtHostContext, + @IAuthenticationService private readonly authenticationService: IAuthenticationService, + ) { + super(); + this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostAuthentication); + } + + $registerAuthenticationProvider(id: string): void { + const provider = new MainThreadAuthenticationProvider(this._proxy, id); + this.authenticationService.registerAuthenticationProvider(id, provider); + } + + $unregisterAuthenticationProvider(id: string): void { + this.authenticationService.unregisterAuthenticationProvider(id); + } + + $onDidChangeSessions(id: string) { + this.authenticationService.sessionsUpdate(id); + } +} diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index 4495e90366..f9bbd9a0b4 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -154,7 +154,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb return Promise.resolve(); } - public $registerDebugConfigurationProvider(debugType: string, hasProvide: boolean, hasResolve: boolean, hasProvideDebugAdapter: boolean, handle: number): Promise { + public $registerDebugConfigurationProvider(debugType: string, hasProvide: boolean, hasResolve: boolean, hasResolve2: boolean, hasProvideDebugAdapter: boolean, handle: number): Promise { const provider = { type: debugType @@ -169,6 +169,11 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb return this._proxy.$resolveDebugConfiguration(handle, folder, config, token); }; } + if (hasResolve2) { + provider.resolveDebugConfigurationWithSubstitutedVariables = (folder, config, token) => { + return this._proxy.$resolveDebugConfigurationWithSubstitutedVariables(handle, folder, config, token); + }; + } if (hasProvideDebugAdapter) { console.info('DebugConfigurationProvider.debugAdapterExecutable is deprecated and will be removed soon; please use DebugAdapterDescriptorFactory.createDebugAdapterDescriptor instead.'); provider.debugAdapterExecutable = (folder) => { diff --git a/src/vs/workbench/api/browser/mainThreadDocuments.ts b/src/vs/workbench/api/browser/mainThreadDocuments.ts index 5fcdc4c612..0417bf1139 100644 --- a/src/vs/workbench/api/browser/mainThreadDocuments.ts +++ b/src/vs/workbench/api/browser/mainThreadDocuments.ts @@ -8,15 +8,13 @@ import { IDisposable, IReference, dispose, DisposableStore } from 'vs/base/commo import { Schemas } from 'vs/base/common/network'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ITextModel } from 'vs/editor/common/model'; -import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService, shouldSynchronizeModel } from 'vs/editor/common/services/modelService'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IFileService } from 'vs/platform/files/common/files'; import { MainThreadDocumentsAndEditors } from 'vs/workbench/api/browser/mainThreadDocumentsAndEditors'; import { ExtHostContext, ExtHostDocumentsShape, IExtHostContext, MainThreadDocumentsShape } from 'vs/workbench/api/common/extHost.protocol'; import { ITextEditorModel } from 'vs/workbench/common/editor'; -import { ITextFileService, TextFileModelChangeEvent } from 'vs/workbench/services/textfile/common/textfiles'; -import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { toLocalResource } from 'vs/base/common/resources'; @@ -70,7 +68,6 @@ export class MainThreadDocuments implements MainThreadDocumentsShape { private readonly _textModelResolverService: ITextModelService; private readonly _textFileService: ITextFileService; private readonly _fileService: IFileService; - private readonly _untitledTextEditorService: IUntitledTextEditorService; private readonly _environmentService: IWorkbenchEnvironmentService; private readonly _toDispose = new DisposableStore(); @@ -83,18 +80,15 @@ export class MainThreadDocuments implements MainThreadDocumentsShape { documentsAndEditors: MainThreadDocumentsAndEditors, extHostContext: IExtHostContext, @IModelService modelService: IModelService, - @IModeService modeService: IModeService, @ITextFileService textFileService: ITextFileService, @IFileService fileService: IFileService, @ITextModelService textModelResolverService: ITextModelService, - @IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService ) { this._modelService = modelService; this._textModelResolverService = textModelResolverService; this._textFileService = textFileService; this._fileService = fileService; - this._untitledTextEditorService = untitledTextEditorService; this._environmentService = environmentService; this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocuments); @@ -104,19 +98,14 @@ export class MainThreadDocuments implements MainThreadDocumentsShape { this._toDispose.add(this._modelReferenceCollection); this._toDispose.add(modelService.onModelModeChanged(this._onModelModeChanged, this)); - this._toDispose.add(textFileService.models.onModelSaved(e => { - if (this._shouldHandleFileEvent(e)) { - this._proxy.$acceptModelSaved(e.resource); + this._toDispose.add(textFileService.files.onDidSave(e => { + if (this._shouldHandleFileEvent(e.model.resource)) { + this._proxy.$acceptModelSaved(e.model.resource); } })); - this._toDispose.add(textFileService.models.onModelReverted(e => { - if (this._shouldHandleFileEvent(e)) { - this._proxy.$acceptDirtyStateChanged(e.resource, false); - } - })); - this._toDispose.add(textFileService.models.onModelDirty(e => { - if (this._shouldHandleFileEvent(e)) { - this._proxy.$acceptDirtyStateChanged(e.resource, true); + this._toDispose.add(textFileService.files.onDidChangeDirty(m => { + if (this._shouldHandleFileEvent(m.resource)) { + this._proxy.$acceptDirtyStateChanged(m.resource, m.isDirty()); } })); @@ -131,8 +120,8 @@ export class MainThreadDocuments implements MainThreadDocumentsShape { this._toDispose.dispose(); } - private _shouldHandleFileEvent(e: TextFileModelChangeEvent): boolean { - const model = this._modelService.getModel(e.resource); + private _shouldHandleFileEvent(resource: URI): boolean { + const model = this._modelService.getModel(resource); return !!model && shouldSynchronizeModel(model); } @@ -222,16 +211,15 @@ export class MainThreadDocuments implements MainThreadDocumentsShape { // don't create a new file ontop of an existing file return Promise.reject(new Error('file already exists')); }, err => { - return this._doCreateUntitled(uri).then(resource => !!resource); + return this._doCreateUntitled(Boolean(uri.path) ? uri : undefined).then(resource => !!resource); }); } - private _doCreateUntitled(resource?: URI, mode?: string, initialValue?: string): Promise { - return this._untitledTextEditorService.loadOrCreate({ - resource, + private _doCreateUntitled(associatedResource?: URI, mode?: string, initialValue?: string): Promise { + return this._textFileService.untitled.resolve({ + associatedResource, mode, - initialValue, - useResourcePath: Boolean(resource && resource.path) + initialValue }).then(model => { const resource = model.resource; diff --git a/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts b/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts index ffdbdec8e2..66603f5ad7 100644 --- a/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts @@ -12,7 +12,6 @@ import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IEditor } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; -import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService, shouldSynchronizeModel } from 'vs/editor/common/services/modelService'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IFileService } from 'vs/platform/files/common/files'; @@ -28,7 +27,6 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; namespace delta { @@ -324,10 +322,8 @@ export class MainThreadDocumentsAndEditors { @ITextFileService private readonly _textFileService: ITextFileService, @IEditorService private readonly _editorService: IEditorService, @ICodeEditorService codeEditorService: ICodeEditorService, - @IModeService modeService: IModeService, @IFileService fileService: IFileService, @ITextModelService textModelResolverService: ITextModelService, - @IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorService, @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, @IBulkEditService bulkEditService: IBulkEditService, @IPanelService panelService: IPanelService, @@ -335,7 +331,7 @@ export class MainThreadDocumentsAndEditors { ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocumentsAndEditors); - const mainThreadDocuments = this._toDispose.add(new MainThreadDocuments(this, extHostContext, this._modelService, modeService, this._textFileService, fileService, textModelResolverService, untitledTextEditorService, environmentService)); + const mainThreadDocuments = this._toDispose.add(new MainThreadDocuments(this, extHostContext, this._modelService, this._textFileService, fileService, textModelResolverService, environmentService)); extHostContext.set(MainContext.MainThreadDocuments, mainThreadDocuments); const mainThreadTextEditors = this._toDispose.add(new MainThreadTextEditors(this, extHostContext, codeEditorService, bulkEditService, this._editorService, this._editorGroupService)); diff --git a/src/vs/workbench/api/browser/mainThreadEditor.ts b/src/vs/workbench/api/browser/mainThreadEditor.ts index 929bc8f9ca..9e9b190442 100644 --- a/src/vs/workbench/api/browser/mainThreadEditor.ts +++ b/src/vs/workbench/api/browser/mainThreadEditor.ts @@ -9,7 +9,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { RenderLineNumbersType, TextEditorCursorStyle, cursorStyleToString, EditorOption } from 'vs/editor/common/config/editorOptions'; import { IRange, Range } from 'vs/editor/common/core/range'; import { ISelection, Selection } from 'vs/editor/common/core/selection'; -import * as editorCommon from 'vs/editor/common/editorCommon'; +import { IDecorationOptions, ScrollType } from 'vs/editor/common/editorCommon'; import { IIdentifiedSingleEditOperation, ISingleEditOperation, ITextModel, ITextModelUpdateOptions } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/modelService'; import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; @@ -365,7 +365,7 @@ export class MainThreadTextEditor { } } - public setDecorations(key: string, ranges: editorCommon.IDecorationOptions[]): void { + public setDecorations(key: string, ranges: IDecorationOptions[]): void { if (!this._codeEditor) { return; } @@ -389,16 +389,16 @@ export class MainThreadTextEditor { } switch (revealType) { case TextEditorRevealType.Default: - this._codeEditor.revealRange(range, editorCommon.ScrollType.Smooth); + this._codeEditor.revealRange(range, ScrollType.Smooth); break; case TextEditorRevealType.InCenter: - this._codeEditor.revealRangeInCenter(range, editorCommon.ScrollType.Smooth); + this._codeEditor.revealRangeInCenter(range, ScrollType.Smooth); break; case TextEditorRevealType.InCenterIfOutsideViewport: - this._codeEditor.revealRangeInCenterIfOutsideViewport(range, editorCommon.ScrollType.Smooth); + this._codeEditor.revealRangeInCenterIfOutsideViewport(range, ScrollType.Smooth); break; case TextEditorRevealType.AtTop: - this._codeEditor.revealRangeAtTop(range, editorCommon.ScrollType.Smooth); + this._codeEditor.revealRangeAtTop(range, ScrollType.Smooth); break; default: console.warn(`Unknown revealType: ${revealType}`); diff --git a/src/vs/workbench/api/browser/mainThreadEditors.ts b/src/vs/workbench/api/browser/mainThreadEditors.ts index 42a721c3c9..d5ad4563c6 100644 --- a/src/vs/workbench/api/browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadEditors.ts @@ -217,7 +217,7 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { $tryApplyWorkspaceEdit(dto: IWorkspaceEditDto): Promise { const { edits } = reviveWorkspaceEditDto(dto); - return this._bulkEditService.apply({ edits }, undefined).then(() => true, err => false); + return this._bulkEditService.apply({ edits }).then(() => true, _err => false); } $tryInsertSnippet(id: string, template: string, ranges: readonly IRange[], opts: IUndoStopOptions): Promise { diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index c04a92c7fc..7aeee549ac 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -327,8 +327,12 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha // --- semantic tokens - $registerSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: modes.SemanticTokensLegend): void { - this._registrations.set(handle, modes.SemanticTokensProviderRegistry.register(selector, new MainThreadSemanticTokensProvider(this._proxy, handle, legend))); + $registerDocumentSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: modes.SemanticTokensLegend): void { + this._registrations.set(handle, modes.DocumentSemanticTokensProviderRegistry.register(selector, new MainThreadDocumentSemanticTokensProvider(this._proxy, handle, legend))); + } + + $registerDocumentRangeSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: modes.SemanticTokensLegend): void { + this._registrations.set(handle, modes.DocumentRangeSemanticTokensProviderRegistry.register(selector, new MainThreadDocumentRangeSemanticTokensProvider(this._proxy, handle, legend))); } // --- suggest @@ -368,6 +372,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha return { suggestions: result.b.map(d => MainThreadLanguageFeatures._inflateSuggestDto(result.a, d)), incomplete: result.c, + isDetailsResolved: result.d, dispose: () => typeof result.x === 'number' && this._proxy.$releaseCompletionItems(handle, result.x) }; }); @@ -607,7 +612,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha } -export class MainThreadSemanticTokensProvider implements modes.SemanticTokensProvider { +export class MainThreadDocumentSemanticTokensProvider implements modes.DocumentSemanticTokensProvider { constructor( private readonly _proxy: ExtHostLanguageFeaturesShape, @@ -616,9 +621,9 @@ export class MainThreadSemanticTokensProvider implements modes.SemanticTokensPro ) { } - public releaseSemanticTokens(resultId: string | undefined): void { + public releaseDocumentSemanticTokens(resultId: string | undefined): void { if (resultId) { - this._proxy.$releaseSemanticTokens(this._handle, parseInt(resultId, 10)); + this._proxy.$releaseDocumentSemanticTokens(this._handle, parseInt(resultId, 10)); } } @@ -626,9 +631,9 @@ export class MainThreadSemanticTokensProvider implements modes.SemanticTokensPro return this._legend; } - async provideSemanticTokens(model: ITextModel, lastResultId: string | null, ranges: EditorRange[] | null, token: CancellationToken): Promise { + async provideDocumentSemanticTokens(model: ITextModel, lastResultId: string | null, token: CancellationToken): Promise { const nLastResultId = lastResultId ? parseInt(lastResultId, 10) : 0; - const encodedDto = await this._proxy.$provideSemanticTokens(this._handle, model.uri, ranges, nLastResultId, token); + const encodedDto = await this._proxy.$provideDocumentSemanticTokens(this._handle, model.uri, nLastResultId, token); if (!encodedDto) { return null; } @@ -648,3 +653,35 @@ export class MainThreadSemanticTokensProvider implements modes.SemanticTokensPro }; } } + +export class MainThreadDocumentRangeSemanticTokensProvider implements modes.DocumentRangeSemanticTokensProvider { + + constructor( + private readonly _proxy: ExtHostLanguageFeaturesShape, + private readonly _handle: number, + private readonly _legend: modes.SemanticTokensLegend, + ) { + } + + public getLegend(): modes.SemanticTokensLegend { + return this._legend; + } + + async provideDocumentRangeSemanticTokens(model: ITextModel, range: EditorRange, token: CancellationToken): Promise { + const encodedDto = await this._proxy.$provideDocumentRangeSemanticTokens(this._handle, model.uri, range, token); + if (!encodedDto) { + return null; + } + if (token.isCancellationRequested) { + return null; + } + const dto = decodeSemanticTokensDto(encodedDto); + if (dto.type === 'full') { + return { + resultId: String(dto.id), + data: dto.data + }; + } + throw new Error(`Unexpected`); + } +} diff --git a/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts b/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts index 87ab712719..3abaf6c2ae 100644 --- a/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts +++ b/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts @@ -3,11 +3,10 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IdleValue, sequence } from 'vs/base/common/async'; +import { IdleValue } from 'vs/base/common/async'; import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; import * as strings from 'vs/base/common/strings'; import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { trimTrailingWhitespace } from 'vs/editor/common/commands/trimTrailingWhitespaceCommand'; import { EditOperation } from 'vs/editor/common/core/editOperation'; @@ -15,7 +14,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 { ITextModel } from 'vs/editor/common/model'; -import { CodeAction } from 'vs/editor/common/modes'; +import { CodeAction, CodeActionTriggerType } from 'vs/editor/common/modes'; import { shouldSynchronizeModel } from 'vs/editor/common/services/modelService'; import { getCodeActions } from 'vs/editor/contrib/codeAction/codeAction'; import { applyCodeAction } from 'vs/editor/contrib/codeAction/codeActionCommands'; @@ -23,22 +22,34 @@ import { CodeActionKind } from 'vs/editor/contrib/codeAction/types'; import { formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format'; import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; import { localize } from 'vs/nls'; -import { ICommandService } from 'vs/platform/commands/common/commands'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { IProgressService, ProgressLocation, IProgressStep, IProgress } from 'vs/platform/progress/common/progress'; import { extHostCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { ISaveParticipant, IResolvedTextFileEditorModel, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; import { SaveReason } from 'vs/workbench/common/editor'; import { ExtHostContext, ExtHostDocumentSaveParticipantShape, IExtHostContext } from '../common/extHost.protocol'; import { INotebookService } from 'sql/workbench/services/notebook/browser/notebookService'; // {{SQL CARBON EDIT}} +import { IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/common/statusbar'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { SettingsEditor2 } from 'vs/workbench/contrib/preferences/browser/settingsEditor2'; +import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { canceled, isPromiseCanceledError } from 'vs/base/common/errors'; export interface ICodeActionsOnSaveOptions { [kind: string]: boolean; } +class SaveParticipantError extends Error { + constructor(message: string, readonly setting?: string) { + super(message); + } +} + /* * An update participant that ensures any un-tracked changes are synced to the JSON file contents for a * Notebook before save occurs. While every effort is made to ensure model changes are notified and a listener @@ -63,8 +74,8 @@ class NotebookUpdateParticipant implements ISaveParticipantParticipant { // {{SQ } } -export interface ISaveParticipantParticipant extends ISaveParticipant { - // progressMessage: string; +export interface ISaveParticipantParticipant { + participate(model: IResolvedTextFileEditorModel, env: { reason: SaveReason }, progress: IProgress, token: CancellationToken): Promise; } class TrimWhitespaceParticipant implements ISaveParticipantParticipant { @@ -107,7 +118,7 @@ class TrimWhitespaceParticipant implements ISaveParticipantParticipant { return; // Nothing to do } - model.pushEditOperations(prevSelection, ops, (edits) => prevSelection); + model.pushEditOperations(prevSelection, ops, (_edits) => prevSelection); } } @@ -138,7 +149,7 @@ export class FinalNewLineParticipant implements ISaveParticipantParticipant { // Nothing } - async participate(model: IResolvedTextFileEditorModel, env: { reason: SaveReason; }): Promise { + async participate(model: IResolvedTextFileEditorModel, _env: { reason: SaveReason; }): Promise { if (this.configurationService.getValue('files.insertFinalNewline', { overrideIdentifier: model.textEditorModel.getLanguageIdentifier().language, resource: model.resource })) { this.doInsertFinalNewLine(model.textEditorModel); } @@ -224,7 +235,7 @@ export class TrimFinalNewLinesParticipant implements ISaveParticipantParticipant return; } - model.pushEditOperations(prevSelection, [EditOperation.delete(deletionRange)], edits => prevSelection); + model.pushEditOperations(prevSelection, [EditOperation.delete(deletionRange)], _edits => prevSelection); if (editor) { editor.setSelections(prevSelection); @@ -242,7 +253,7 @@ class FormatOnSaveParticipant implements ISaveParticipantParticipant { // Nothing } - async participate(editorModel: IResolvedTextFileEditorModel, env: { reason: SaveReason; }): Promise { + async participate(editorModel: IResolvedTextFileEditorModel, env: { reason: SaveReason; }, progress: IProgress, token: CancellationToken): Promise { const model = editorModel.textEditorModel; const overrides = { overrideIdentifier: model.getLanguageIdentifier().language, resource: model.uri }; @@ -251,32 +262,20 @@ class FormatOnSaveParticipant implements ISaveParticipantParticipant { return undefined; } - return new Promise((resolve, reject) => { - const source = new CancellationTokenSource(); - const editorOrModel = findEditor(model, this._codeEditorService) || model; - const timeout = this._configurationService.getValue('editor.formatOnSaveTimeout', overrides); - const request = this._instantiationService.invokeFunction(formatDocumentWithSelectedProvider, editorOrModel, FormattingMode.Silent, source.token); - - setTimeout(() => { - reject(localize('timeout.formatOnSave', "Aborted format on save after {0}ms", timeout)); - source.cancel(); - }, timeout); - - request.then(resolve, reject); - }); + progress.report({ message: localize('formatting', "Formatting") }); + const editorOrModel = findEditor(model, this._codeEditorService) || model; + await this._instantiationService.invokeFunction(formatDocumentWithSelectedProvider, editorOrModel, FormattingMode.Silent, token); } } -class CodeActionOnSaveParticipant implements ISaveParticipant { +class CodeActionOnSaveParticipant implements ISaveParticipantParticipant { constructor( - @IBulkEditService private readonly _bulkEditService: IBulkEditService, - @ICommandService private readonly _commandService: ICommandService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { } - async participate(editorModel: IResolvedTextFileEditorModel, env: { reason: SaveReason; }): Promise { + async participate(editorModel: IResolvedTextFileEditorModel, env: { reason: SaveReason; }, progress: IProgress, token: CancellationToken): Promise { if (env.reason === SaveReason.AUTO) { return undefined; } @@ -312,20 +311,8 @@ class CodeActionOnSaveParticipant implements ISaveParticipant { .filter(x => setting[x] === false) .map(x => new CodeActionKind(x)); - const tokenSource = new CancellationTokenSource(); - - const timeout = this._configurationService.getValue('editor.codeActionsOnSaveTimeout', settingsOverrides); - - return Promise.race([ - new Promise((_resolve, reject) => - setTimeout(() => { - tokenSource.cancel(); - reject(localize('codeActionsOnSave.didTimeout', "Aborted codeActionsOnSave after {0}ms", timeout)); - }, timeout)), - this.applyOnSaveActions(model, codeActionsOnSave, excludedActions, tokenSource.token) - ]).finally(() => { - tokenSource.cancel(); - }); + progress.report({ message: localize('codeaction', "Quick Fixes") }); + await this.applyOnSaveActions(model, codeActionsOnSave, excludedActions, token); } private async applyOnSaveActions(model: ITextModel, codeActionsOnSave: readonly CodeActionKind[], excludes: readonly CodeActionKind[], token: CancellationToken): Promise { @@ -343,13 +330,13 @@ class CodeActionOnSaveParticipant implements ISaveParticipant { private async applyCodeActions(actionsToRun: readonly CodeAction[]) { for (const action of actionsToRun) { - await this._instantiationService.invokeFunction(applyCodeAction, action, this._bulkEditService, this._commandService); + await this._instantiationService.invokeFunction(applyCodeAction, action); } } private getActionsToRun(model: ITextModel, codeActionKind: CodeActionKind, excludes: readonly CodeActionKind[], token: CancellationToken) { return getCodeActions(model, model.getFullModelRange(), { - type: 'auto', + type: CodeActionTriggerType.Auto, filter: { include: codeActionKind, excludes: excludes, includeSourceActions: true }, }, token); } @@ -363,7 +350,7 @@ class ExtHostSaveParticipant implements ISaveParticipantParticipant { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocumentSaveParticipant); } - async participate(editorModel: IResolvedTextFileEditorModel, env: { reason: SaveReason; }): Promise { + async participate(editorModel: IResolvedTextFileEditorModel, env: { reason: SaveReason; }, _progress: IProgress, token: CancellationToken): Promise { if (!shouldSynchronizeModel(editorModel.textEditorModel)) { // the model never made it to the extension @@ -372,7 +359,13 @@ class ExtHostSaveParticipant implements ISaveParticipantParticipant { } return new Promise((resolve, reject) => { - setTimeout(() => reject(localize('timeout.onWillSave', "Aborted onWillSaveTextDocument-event after 1750ms")), 1750); + + token.onCancellationRequested(() => reject(canceled())); + + setTimeout( + () => reject(new SaveParticipantError(localize('timeout.onWillSave', "Aborted onWillSaveTextDocument-event after 1750ms"))), + 1750 + ); this._proxy.$participateInSave(editorModel.resource, env.reason).then(values => { if (!values.every(success => success)) { return Promise.reject(new Error('listener failed')); @@ -393,7 +386,9 @@ export class SaveParticipant implements ISaveParticipant { extHostContext: IExtHostContext, @IInstantiationService instantiationService: IInstantiationService, @IProgressService private readonly _progressService: IProgressService, - @ILogService private readonly _logService: ILogService + @IStatusbarService private readonly _statusbarService: IStatusbarService, + @ILogService private readonly _logService: ILogService, + @ILabelService private readonly _labelService: ILabelService, ) { this._saveParticipants = new IdleValue(() => [ instantiationService.createInstance(TrimWhitespaceParticipant), @@ -415,12 +410,72 @@ export class SaveParticipant implements ISaveParticipant { } async participate(model: IResolvedTextFileEditorModel, env: { reason: SaveReason; }): Promise { - return this._progressService.withProgress({ location: ProgressLocation.Window }, progress => { - progress.report({ message: localize('saveParticipants', "Running Save Participants...") }); - const promiseFactory = this._saveParticipants.getValue().map(p => () => { - return p.participate(model, env); - }); - return sequence(promiseFactory).then(() => { }, err => this._logService.warn(err)); + + const cts = new CancellationTokenSource(); + + return this._progressService.withProgress({ + title: localize('saveParticipants', "Running Save Participants for '{0}'", this._labelService.getUriLabel(model.resource, { relative: true })), + location: ProgressLocation.Notification, + cancellable: true, + delay: model.isDirty() ? 3000 : 5000 + }, async progress => { + + let firstError: SaveParticipantError | undefined; + for (let p of this._saveParticipants.getValue()) { + + if (cts.token.isCancellationRequested) { + break; + } + + try { + await p.participate(model, env, progress, cts.token); + + } catch (err) { + if (!isPromiseCanceledError(err)) { + this._logService.warn(err); + firstError = !firstError && err instanceof SaveParticipantError ? err : firstError; + } + } + } + + if (firstError) { + this._showParticipantError(firstError); + } + + }, () => { + // user cancel + cts.dispose(true); }); } + + private _showParticipantError(err: SaveParticipantError): void { + + let entry: any = { + text: localize('title', "$(error) Save Participants Failed: {0}", err.message) + }; + if (err.setting) { + entry.command = '_showSettings'; + entry.arguments = [err.setting]; + } + + const handle = this._statusbarService.addEntry( + entry, + 'saveParticipants.error', + localize('status.message', "Save Participants Errors"), + StatusbarAlignment.LEFT, + -Number.MAX_VALUE /* far right on left hand side */ + ); + + setTimeout(() => handle.dispose(), 5000); + } } + +CommandsRegistry.registerCommand('_showSettings', (accessor, ...args: any[]) => { + const [setting] = args; + const control = accessor.get(IEditorService).activeControl as SettingsEditor2; + if (control instanceof SettingsEditor2) { + control.focusSearch(`@tag:usesOnlineServices`); + } else { + accessor.get(IPreferencesService).openSettings(false, setting); + } +}); diff --git a/src/vs/workbench/api/browser/mainThreadTask.ts b/src/vs/workbench/api/browser/mainThreadTask.ts index 1cd2760c4a..069644a341 100644 --- a/src/vs/workbench/api/browser/mainThreadTask.ts +++ b/src/vs/workbench/api/browser/mainThreadTask.ts @@ -621,7 +621,10 @@ export class MainThreadTask implements MainThreadTaskShape { for (let i = 0; i < partiallyResolvedVars.length; i++) { const variableName = vars[i].substring(2, vars[i].length - 1); if (resolvedVars && values.variables[vars[i]] === vars[i]) { - result.variables.set(variableName, resolvedVars.get(variableName)); + const resolved = resolvedVars.get(variableName); + if (typeof resolved === 'string') { + result.variables.set(variableName, resolved); + } } else { result.variables.set(variableName, partiallyResolvedVars[i]); } diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index 1357e5f219..06403d89af 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -343,7 +343,7 @@ class TerminalDataEventTracker extends Disposable { ) { super(); - this._register(this._bufferer = new TerminalDataBufferer()); + this._register(this._bufferer = new TerminalDataBufferer(this._callback)); this._terminalService.terminalInstances.forEach(instance => this._registerInstance(instance)); this._register(this._terminalService.onInstanceCreated(instance => this._registerInstance(instance))); @@ -352,6 +352,6 @@ class TerminalDataEventTracker extends Disposable { private _registerInstance(instance: ITerminalInstance): void { // Buffer data events to reduce the amount of messages going to the extension host - this._register(this._bufferer.startBuffering(instance.id, instance.onData, this._callback)); + this._register(this._bufferer.startBuffering(instance.id, instance.onData)); } } diff --git a/src/vs/workbench/api/browser/mainThreadTunnelService.ts b/src/vs/workbench/api/browser/mainThreadTunnelService.ts index 936d7d10f5..fbd465879e 100644 --- a/src/vs/workbench/api/browser/mainThreadTunnelService.ts +++ b/src/vs/workbench/api/browser/mainThreadTunnelService.ts @@ -22,7 +22,7 @@ export class MainThreadTunnelService implements MainThreadTunnelServiceShape { } async $openTunnel(tunnelOptions: TunnelOptions): Promise { - const tunnel = await this.remoteExplorerService.forward(tunnelOptions.remoteAddress, tunnelOptions.localPort, tunnelOptions.label); + const tunnel = await this.remoteExplorerService.forward(tunnelOptions.remoteAddress, tunnelOptions.localAddressPort, tunnelOptions.label); if (tunnel) { return TunnelDto.fromServiceTunnel(tunnel); } diff --git a/src/vs/workbench/api/browser/mainThreadWebview.ts b/src/vs/workbench/api/browser/mainThreadWebview.ts index 75684f6708..85a9b04b8d 100644 --- a/src/vs/workbench/api/browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/browser/mainThreadWebview.ts @@ -12,6 +12,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import * as modes from 'vs/editor/common/modes'; import { localize } from 'vs/nls'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { IFileService } from 'vs/platform/files/common/files'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IProductService } from 'vs/platform/product/common/productService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -107,6 +108,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma @IProductService private readonly _productService: IProductService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @IWebviewWorkbenchService private readonly _webviewWorkbenchService: IWebviewWorkbenchService, + @IFileService private readonly _fileService: IFileService, ) { super(); @@ -307,7 +309,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma } private async retainCustomEditorModel(webviewInput: WebviewInput, resource: URI, viewType: string, capabilities: readonly extHostProtocol.WebviewEditorCapabilities[]) { - const model = await this._customEditorService.models.loadOrCreate(webviewInput.getResource(), webviewInput.viewType); + const model = await this._customEditorService.models.resolve(webviewInput.getResource(), webviewInput.viewType); const existingEntry = this._customEditorModels.get(model); if (existingEntry) { @@ -319,7 +321,8 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma this._customEditorModels.set(model, { referenceCount: 1 }); const capabilitiesSet = new Set(capabilities); - if (capabilitiesSet.has(extHostProtocol.WebviewEditorCapabilities.Editable)) { + const isEditable = capabilitiesSet.has(extHostProtocol.WebviewEditorCapabilities.Editable); + if (isEditable) { model.onUndo(edits => { this._proxy.$undoEdits(resource, viewType, edits.map(x => x.data)); }); @@ -335,10 +338,17 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma e.waitUntil(this._proxy.$onSave(resource.toJSON(), viewType)); }); - model.onWillSaveAs(e => { - e.waitUntil(this._proxy.$onSaveAs(e.resource.toJSON(), viewType, e.targetResource.toJSON())); - }); } + + // Save as should always be implemented even if the model is readonly + model.onWillSaveAs(e => { + if (isEditable) { + e.waitUntil(this._proxy.$onSaveAs(e.resource.toJSON(), viewType, e.targetResource.toJSON())); + } else { + // Since the editor is readonly, just copy the file over + e.waitUntil(this._fileService.copy(e.resource, e.targetResource, false /* overwrite */)); + } + }); return model; } diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index 3ca8f0a5d1..f5281c6e4d 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -36,6 +36,7 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; export interface IUserFriendlyViewsContainerDescriptor { id: string; @@ -332,7 +333,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution { viewContainer = this.viewContainersRegistry.registerViewContainer({ id, name: title, extensionId, - ctorDescriptor: { ctor: CustomViewPaneContainer }, + ctorDescriptor: new SyncDescriptor(CustomViewPaneContainer), hideIfEmpty: true, icon, }, ViewContainerLocation.Sidebar); @@ -416,7 +417,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution { const viewDescriptor = { id: item.id, name: item.name, - ctorDescriptor: { ctor: CustomTreeViewPane }, + ctorDescriptor: new SyncDescriptor(CustomTreeViewPane), when: ContextKeyExpr.deserialize(item.when), canToggleVisibility: true, collapsed: this.showCollapsed(container), diff --git a/src/vs/workbench/api/common/apiCommands.ts b/src/vs/workbench/api/common/apiCommands.ts index c8c79c7416..56a1c5e19a 100644 --- a/src/vs/workbench/api/common/apiCommands.ts +++ b/src/vs/workbench/api/common/apiCommands.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { URI } from 'vs/base/common/uri'; import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { CommandsRegistry, ICommandService, ICommandHandler } from 'vs/platform/commands/common/commands'; @@ -14,6 +14,8 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { IOpenWindowOptions, IWindowOpenable, IOpenEmptyWindowOptions } from 'vs/platform/windows/common/windows'; import { IWorkspacesService, hasWorkspaceFileExtension, IRecent } from 'vs/platform/workspaces/common/workspaces'; import { Schemas } from 'vs/base/common/network'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; // ----------------------------------------------------------------- // The following commands are registered on both sides separately. @@ -132,6 +134,28 @@ export class OpenAPICommand { } CommandsRegistry.registerCommand(OpenAPICommand.ID, adjustHandler(OpenAPICommand.execute)); +export class OpenWithAPICommand { + public static readonly ID = 'vscode.openWith'; + public static execute(executor: ICommandsExecutor, resource: URI, viewType: string, columnOrOptions?: vscode.ViewColumn | vscode.TextDocumentShowOptions): Promise { + let options: ITextEditorOptions | undefined; + let position: EditorViewColumn | undefined; + + if (typeof columnOrOptions === 'number') { + position = typeConverters.ViewColumn.from(columnOrOptions); + } else if (typeof columnOrOptions !== 'undefined') { + options = typeConverters.TextEditorOptions.from(columnOrOptions); + } + + return executor.executeCommand('_workbench.openWith', [ + resource, + viewType, + options, + position + ]); + } +} +CommandsRegistry.registerCommand(OpenWithAPICommand.ID, adjustHandler(OpenWithAPICommand.execute)); + CommandsRegistry.registerCommand('_workbench.removeFromRecentlyOpened', function (accessor: ServicesAccessor, uri: URI) { const workspacesService = accessor.get(IWorkspacesService); return workspacesService.removeFromRecentlyOpened([uri]); @@ -215,3 +239,18 @@ CommandsRegistry.registerCommand({ }] } }); + +CommandsRegistry.registerCommand('_extensionTests.setLogLevel', function (accessor: ServicesAccessor, level: number) { + const logService = accessor.get(ILogService); + const environmentService = accessor.get(IEnvironmentService); + + if (environmentService.isExtensionDevelopment && !!environmentService.extensionTestsLocationURI) { + logService.setLevel(level); + } +}); + +CommandsRegistry.registerCommand('_extensionTests.getLogLevel', function (accessor: ServicesAccessor) { + const logService = accessor.get(ILogService); + + return logService.getLevel(); +}); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 1470720e40..c3381c1459 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -70,6 +70,8 @@ import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitData import { find } from 'vs/base/common/arrays'; import { ExtHostTheming } from 'vs/workbench/api/common/extHostTheming'; import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService'; +import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; +import { ExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication'; export interface IExtensionApiFactory { (extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode; @@ -90,6 +92,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostStorage = accessor.get(IExtHostStorage); const extHostLogService = accessor.get(ILogService); const extHostTunnelService = accessor.get(IExtHostTunnelService); + const extHostApiDeprecation = accessor.get(IExtHostApiDeprecationService); // register addressable instances rpcProtocol.set(ExtHostContext.ExtHostLogService, extHostLogService); @@ -119,7 +122,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostTreeViews = rpcProtocol.set(ExtHostContext.ExtHostTreeViews, new ExtHostTreeViews(rpcProtocol.getProxy(MainContext.MainThreadTreeViews), extHostCommands, extHostLogService)); const extHostEditorInsets = rpcProtocol.set(ExtHostContext.ExtHostEditorInsets, new ExtHostEditorInsets(rpcProtocol.getProxy(MainContext.MainThreadEditorInsets), extHostEditors, initData.environment)); const extHostDiagnostics = rpcProtocol.set(ExtHostContext.ExtHostDiagnostics, new ExtHostDiagnostics(rpcProtocol, extHostLogService)); - const extHostLanguageFeatures = rpcProtocol.set(ExtHostContext.ExtHostLanguageFeatures, new ExtHostLanguageFeatures(rpcProtocol, uriTransformer, extHostDocuments, extHostCommands, extHostDiagnostics, extHostLogService)); + const extHostLanguageFeatures = rpcProtocol.set(ExtHostContext.ExtHostLanguageFeatures, new ExtHostLanguageFeatures(rpcProtocol, uriTransformer, extHostDocuments, extHostCommands, extHostDiagnostics, extHostLogService, extHostApiDeprecation)); const extHostFileSystem = rpcProtocol.set(ExtHostContext.ExtHostFileSystem, new ExtHostFileSystem(rpcProtocol, extHostLanguageFeatures)); const extHostFileSystemEvent = rpcProtocol.set(ExtHostContext.ExtHostFileSystemEventService, new ExtHostFileSystemEventService(rpcProtocol, extHostLogService, extHostDocumentsAndEditors)); const extHostQuickOpen = rpcProtocol.set(ExtHostContext.ExtHostQuickOpen, new ExtHostQuickOpen(rpcProtocol, extHostWorkspace, extHostCommands)); @@ -129,6 +132,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostProgress = rpcProtocol.set(ExtHostContext.ExtHostProgress, new ExtHostProgress(rpcProtocol.getProxy(MainContext.MainThreadProgress))); const extHostLabelService = rpcProtocol.set(ExtHostContext.ExtHostLabelService, new ExtHostLabelService(rpcProtocol)); const extHostTheming = rpcProtocol.set(ExtHostContext.ExtHostTheming, new ExtHostTheming(rpcProtocol)); + const extHostAuthentication = rpcProtocol.set(ExtHostContext.ExtHostAuthentication, new ExtHostAuthentication(rpcProtocol)); // Check that no named customers are missing // {{SQL CARBON EDIT}} filter out the services we don't expose @@ -178,6 +182,29 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }; })(); + const authentication: typeof vscode.authentication = { + registerAuthenticationProvider(provider: vscode.AuthenticationProvider): vscode.Disposable { + return extHostAuthentication.registerAuthenticationProvider(provider); + }, + login(providerId: string): Promise { + return extHostAuthentication.$login(providerId); + }, + logout(providerId: string, accountId: string): Promise { + return extHostAuthentication.$logout(providerId, accountId); + }, + getSessions(providerId: string): Promise> { + return extHostAuthentication.$getSessions(providerId); + }, + get onDidChangeSessions() { + return extHostAuthentication.onDidChangeSessions; + }, + get onDidRegisterAuthenticationProvider() { + return extHostAuthentication.onDidRegisterAuthenticationProvider; + }, + get onDidUnregisterAuthenticationProvider() { + return extHostAuthentication.onDidUnregisterAuthenticationProvider; + } + }; // namespace: commands const commands: typeof vscode.commands = { @@ -248,14 +275,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostTerminalService.getDefaultShell(false, configProvider); }, openExternal(uri: URI) { - return extHostWindow.openUri(uri, { allowTunneling: !!initData.remote.isRemote }); + return extHostWindow.openUri(uri, { allowTunneling: !!initData.remote.authority }); }, asExternalUri(uri: URI) { if (uri.scheme === initData.environment.appUriScheme) { return extHostUrls.createAppUri(uri); } - return extHostWindow.asExternalUri(uri, { allowTunneling: !!initData.remote.isRemote }); + return extHostWindow.asExternalUri(uri, { allowTunneling: !!initData.remote.authority }); }, get remoteName() { return getRemoteName(initData.remote.authority); @@ -355,9 +382,13 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerOnTypeFormattingEditProvider(selector: vscode.DocumentSelector, provider: vscode.OnTypeFormattingEditProvider, firstTriggerCharacter: string, ...moreTriggerCharacters: string[]): vscode.Disposable { return extHostLanguageFeatures.registerOnTypeFormattingEditProvider(extension, checkSelector(selector), provider, [firstTriggerCharacter].concat(moreTriggerCharacters)); }, - registerSemanticTokensProvider(selector: vscode.DocumentSelector, provider: vscode.SemanticTokensProvider, legend: vscode.SemanticTokensLegend): vscode.Disposable { + registerDocumentSemanticTokensProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentSemanticTokensProvider, legend: vscode.SemanticTokensLegend): vscode.Disposable { checkProposedApiEnabled(extension); - return extHostLanguageFeatures.registerSemanticTokensProvider(extension, checkSelector(selector), provider, legend); + return extHostLanguageFeatures.registerDocumentSemanticTokensProvider(extension, checkSelector(selector), provider, legend); + }, + registerDocumentRangeSemanticTokensProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentRangeSemanticTokensProvider, legend: vscode.SemanticTokensLegend): vscode.Disposable { + checkProposedApiEnabled(extension); + return extHostLanguageFeatures.registerDocumentRangeSemanticTokensProvider(extension, checkSelector(selector), provider, legend); }, registerSignatureHelpProvider(selector: vscode.DocumentSelector, provider: vscode.SignatureHelpProvider, firstItem?: string | vscode.SignatureHelpProviderMetadata, ...remaining: string[]): vscode.Disposable { if (typeof firstItem === 'object') { @@ -384,7 +415,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostLanguageFeatures.registerCallHierarchyProvider(extension, selector, provider); }, setLanguageConfiguration: (language: string, configuration: vscode.LanguageConfiguration): vscode.Disposable => { - return extHostLanguageFeatures.setLanguageConfiguration(language, configuration); + return extHostLanguageFeatures.setLanguageConfiguration(extension, language, configuration); } }; @@ -507,6 +538,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostStatusBar.setStatusBarMessage(text, timeoutOrThenable); }, withScmProgress(task: (progress: vscode.Progress) => Thenable) { + extHostApiDeprecation.report('window.withScmProgress', extension, + `Use 'withProgress' instead.`); + return extHostProgress.withProgress(extension, { location: extHostTypes.ProgressLocation.SourceControl }, (progress, token) => task({ report(n: number) { /*noop*/ } })); }, withProgress(options: vscode.ProgressOptions, task: (progress: vscode.Progress<{ message?: string; worked?: number }>, token: vscode.CancellationToken) => Thenable) { @@ -568,13 +602,11 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }; // namespace: workspace - let warnedRootPathDeprecated = false; + const workspace: typeof vscode.workspace = { get rootPath() { - if (extension.isUnderDevelopment && !warnedRootPathDeprecated) { - warnedRootPathDeprecated = true; - extHostLogService.warn(`[Deprecation Warning] 'workspace.rootPath' is deprecated and should no longer be used. Please use 'workspace.workspaceFolders' instead. More details: https://aka.ms/vscode-eliminating-rootpath`); - } + extHostApiDeprecation.report('workspace.rootPath', extension, + `Please use 'workspace.workspaceFolders' instead. More details: https://aka.ms/vscode-eliminating-rootpath`); return extHostWorkspace.getPath(); }, @@ -739,6 +771,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // namespace: scm const scm: typeof vscode.scm = { get inputBox() { + extHostApiDeprecation.report('scm.inputBox', extension, + `Use 'SourceControl.inputBox' instead`); + return extHostSCM.getLastInputBox(extension)!; // Strict null override - Deprecated api }, createSourceControl(id: string, label: string, rootUri?: vscode.Uri) { @@ -835,6 +870,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // {{SQL CARBON EDIT}} - Expose the VS Code version here for extensions that rely on it version: initData.vscodeVersion, // namespaces + authentication, commands, debug, env, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 681aebff67..75fea9f5ef 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -151,6 +151,12 @@ export interface MainThreadCommentsShape extends IDisposable { $onDidCommentThreadsChange(handle: number, event: modes.CommentThreadChangedEvent): void; } +export interface MainThreadAuthenticationShape extends IDisposable { + $registerAuthenticationProvider(id: string): void; + $unregisterAuthenticationProvider(id: string): void; + $onDidChangeSessions(id: string): void; +} + export interface MainThreadConfigurationShape extends IDisposable { $updateConfigurationOption(target: ConfigurationTarget | null, key: string, value: any, overrides: IConfigurationOverrides | undefined, scopeToLanguage: boolean | undefined): Promise; $removeConfigurationOption(target: ConfigurationTarget | null, key: string, overrides: IConfigurationOverrides | undefined, scopeToLanguage: boolean | undefined): Promise; @@ -356,7 +362,8 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerOnTypeFormattingSupport(handle: number, selector: IDocumentFilterDto[], autoFormatTriggerCharacters: string[], extensionId: ExtensionIdentifier): void; $registerNavigateTypeSupport(handle: number): void; $registerRenameSupport(handle: number, selector: IDocumentFilterDto[], supportsResolveInitialValues: boolean): void; - $registerSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: modes.SemanticTokensLegend): void; + $registerDocumentSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: modes.SemanticTokensLegend): void; + $registerDocumentRangeSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: modes.SemanticTokensLegend): void; $registerSuggestSupport(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, extensionId: ExtensionIdentifier): void; $registerSignatureHelpProvider(handle: number, selector: IDocumentFilterDto[], metadata: ISignatureHelpProviderMetadataDto): void; $registerDocumentLinkProvider(handle: number, selector: IDocumentFilterDto[], supportsResolve: boolean): void; @@ -754,7 +761,7 @@ export interface MainThreadDebugServiceShape extends IDisposable { $acceptDAMessage(handle: number, message: DebugProtocol.ProtocolMessage): void; $acceptDAError(handle: number, name: string, message: string, stack: string | undefined): void; $acceptDAExit(handle: number, code: number | undefined, signal: string | undefined): void; - $registerDebugConfigurationProvider(type: string, hasProvideMethod: boolean, hasResolveMethod: boolean, hasProvideDaMethod: boolean, handle: number): Promise; + $registerDebugConfigurationProvider(type: string, hasProvideMethod: boolean, hasResolveMethod: boolean, hasResolve2Method: boolean, hasProvideDaMethod: boolean, handle: number): Promise; $registerDebugAdapterDescriptorFactory(type: string, handle: number): Promise; $unregisterDebugConfigurationProvider(handle: number): void; $unregisterDebugAdapterDescriptorFactory(handle: number): void; @@ -896,6 +903,12 @@ export interface ExtHostLabelServiceShape { $registerResourceLabelFormatter(formatter: ResourceLabelFormatter): IDisposable; } +export interface ExtHostAuthenticationShape { + $getSessions(id: string): Promise>; + $login(id: string): Promise; + $logout(id: string, accountId: string): Promise; +} + export interface ExtHostSearchShape { $provideFileSearchResults(handle: number, session: number, query: search.IRawQuery, token: CancellationToken): Promise; $provideTextSearchResults(handle: number, session: number, query: search.IRawTextQuery, token: CancellationToken): Promise; @@ -1000,7 +1013,7 @@ export interface ISuggestDataDto { [ISuggestDataDtoField.documentation]?: string | IMarkdownString; [ISuggestDataDtoField.sortText]?: string; [ISuggestDataDtoField.filterText]?: string; - [ISuggestDataDtoField.preselect]?: boolean; + [ISuggestDataDtoField.preselect]?: true; [ISuggestDataDtoField.insertText]?: string; [ISuggestDataDtoField.insertTextRules]?: modes.CompletionItemInsertTextRule; [ISuggestDataDtoField.range]?: IRange | { insert: IRange, replace: IRange; }; @@ -1016,7 +1029,8 @@ export interface ISuggestResultDto { x?: number; a: { insert: IRange, replace: IRange; }; b: ISuggestDataDto[]; - c?: boolean; + c?: true; + d?: true; } export interface ISignatureHelpDto { @@ -1180,8 +1194,9 @@ export interface ExtHostLanguageFeaturesShape { $releaseWorkspaceSymbols(handle: number, id: number): void; $provideRenameEdits(handle: number, resource: UriComponents, position: IPosition, newName: string, token: CancellationToken): Promise; $resolveRenameLocation(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; - $provideSemanticTokens(handle: number, resource: UriComponents, ranges: IRange[] | null, previousResultId: number, token: CancellationToken): Promise; - $releaseSemanticTokens(handle: number, semanticColoringResultId: number): void; + $provideDocumentSemanticTokens(handle: number, resource: UriComponents, previousResultId: number, token: CancellationToken): Promise; + $releaseDocumentSemanticTokens(handle: number, semanticColoringResultId: number): void; + $provideDocumentRangeSemanticTokens(handle: number, resource: UriComponents, range: IRange, token: CancellationToken): Promise; $provideCompletionItems(handle: number, resource: UriComponents, position: IPosition, context: modes.CompletionContext, token: CancellationToken): Promise; $resolveCompletionItem(handle: number, resource: UriComponents, position: IPosition, id: ChainedCacheId, token: CancellationToken): Promise; $releaseCompletionItems(handle: number, id: number): void; @@ -1342,6 +1357,7 @@ export interface ExtHostDebugServiceShape { $stopDASession(handle: number): Promise; $sendDAMessage(handle: number, message: DebugProtocol.ProtocolMessage): void; $resolveDebugConfiguration(handle: number, folder: UriComponents | undefined, debugConfiguration: IConfig, token: CancellationToken): Promise; + $resolveDebugConfigurationWithSubstitutedVariables(handle: number, folder: UriComponents | undefined, debugConfiguration: IConfig, token: CancellationToken): Promise; $provideDebugConfigurations(handle: number, folder: UriComponents | undefined, token: CancellationToken): Promise; $legacyDebugAdapterExecutable(handle: number, folderUri: UriComponents | undefined): Promise; // TODO@AW legacy $provideDebugAdapter(handle: number, session: IDebugSessionDto): Promise; @@ -1414,6 +1430,7 @@ export interface ExtHostTunnelServiceShape { // --- proxy identifiers export const MainContext = { + MainThreadAuthentication: createMainId('MainThreadAuthentication'), MainThreadClipboard: createMainId('MainThreadClipboard'), MainThreadCommands: createMainId('MainThreadCommands'), MainThreadComments: createMainId('MainThreadComments'), @@ -1489,5 +1506,6 @@ export const ExtHostContext = { ExtHostOutputService: createMainId('ExtHostOutputService'), ExtHostLabelService: createMainId('ExtHostLabelService'), ExtHostTheming: createMainId('ExtHostTheming'), - ExtHostTunnelService: createMainId('ExtHostTunnelService') + ExtHostTunnelService: createMainId('ExtHostTunnelService'), + ExtHostAuthentication: createMainId('ExtHostAuthentication') }; diff --git a/src/vs/workbench/api/common/extHostApiCommands.ts b/src/vs/workbench/api/common/extHostApiCommands.ts index 7dc9312688..e59212b529 100644 --- a/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/src/vs/workbench/api/common/extHostApiCommands.ts @@ -5,7 +5,7 @@ import { URI } from 'vs/base/common/uri'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import * as types from 'vs/workbench/api/common/extHostTypes'; import { IRawColorInfo, IWorkspaceEditDto, ICallHierarchyItemDto, IIncomingCallDto, IOutgoingCallDto } from 'vs/workbench/api/common/extHost.protocol'; @@ -417,7 +417,7 @@ export class ExtHostApiCommands { }; return this._commands.executeCommand('_executeCompletionItemProvider', args).then(result => { if (result) { - const items = result.suggestions.map(suggestion => typeConverters.CompletionItem.to(suggestion)); + const items = result.suggestions.map(suggestion => typeConverters.CompletionItem.to(suggestion, this._commands.converter)); return new types.CompletionList(items, result.incomplete); } return undefined; diff --git a/src/vs/workbench/api/common/extHostApiDeprecationService.ts b/src/vs/workbench/api/common/extHostApiDeprecationService.ts new file mode 100644 index 0000000000..e1a230cef8 --- /dev/null +++ b/src/vs/workbench/api/common/extHostApiDeprecationService.ts @@ -0,0 +1,71 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; +import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; + +export interface IExtHostApiDeprecationService { + _serviceBrand: undefined; + + report(apiId: string, extension: IExtensionDescription, migrationSuggestion: string): void; +} + +export const IExtHostApiDeprecationService = createDecorator('IExtHostApiDeprecationService'); + +export class ExtHostApiDeprecationService implements IExtHostApiDeprecationService { + + _serviceBrand: undefined; + + private readonly _reportedUsages = new Set(); + private readonly _telemetryShape: extHostProtocol.MainThreadTelemetryShape; + + constructor( + @IExtHostRpcService rpc: IExtHostRpcService, + @ILogService private readonly _extHostLogService: ILogService, + ) { + this._telemetryShape = rpc.getProxy(extHostProtocol.MainContext.MainThreadTelemetry); + } + + public report(apiId: string, extension: IExtensionDescription, migrationSuggestion: string): void { + const key = this.getUsageKey(apiId, extension); + if (this._reportedUsages.has(key)) { + return; + } + this._reportedUsages.add(key); + + if (extension.isUnderDevelopment) { + this._extHostLogService.warn(`[Deprecation Warning] '${apiId}' is deprecated. ${migrationSuggestion}`); + } + + type DeprecationTelemetry = { + extensionId: string; + apiId: string; + }; + type DeprecationTelemetryMeta = { + extensionId: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' }; + apiId: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' }; + }; + this._telemetryShape.$publicLog2('extHostDeprecatedApiUsage', { + extensionId: extension.identifier.value, + apiId: apiId, + }); + } + + private getUsageKey(apiId: string, extension: IExtensionDescription): string { + return `${apiId}-${extension.identifier.value}`; + } +} + + +export const NullApiDeprecationService = Object.freeze(new class implements IExtHostApiDeprecationService { + _serviceBrand: undefined; + + public report(_apiId: string, _extension: IExtensionDescription, _warningMessage: string): void { + // noop + } +}()); diff --git a/src/vs/workbench/api/common/extHostAuthentication.ts b/src/vs/workbench/api/common/extHostAuthentication.ts new file mode 100644 index 0000000000..ba805c910e --- /dev/null +++ b/src/vs/workbench/api/common/extHostAuthentication.ts @@ -0,0 +1,101 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as modes from 'vs/editor/common/modes'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IMainContext, MainContext, MainThreadAuthenticationShape, ExtHostAuthenticationShape } from 'vs/workbench/api/common/extHost.protocol'; +import { IDisposable } from 'vs/base/common/lifecycle'; + +const _onDidUnregisterAuthenticationProvider = new Emitter(); +const _onDidChangeSessions = new Emitter(); + +export class ExtHostAuthenticationProvider implements IDisposable { + constructor(private _provider: vscode.AuthenticationProvider, + private _id: string, + private _proxy: MainThreadAuthenticationShape) { + this._provider.onDidChangeSessions(x => { + this._proxy.$onDidChangeSessions(this._id); + _onDidChangeSessions.fire(this._id); + }); + } + + getSessions(): Promise> { + return this._provider.getSessions(); + } + + login(): Promise { + return this._provider.login(); + } + + logout(sessionId: string): Promise { + return this._provider.logout(sessionId); + } + + dispose(): void { + this._proxy.$unregisterAuthenticationProvider(this._id); + _onDidUnregisterAuthenticationProvider.fire(this._id); + } +} + +export class ExtHostAuthentication implements ExtHostAuthenticationShape { + private _proxy: MainThreadAuthenticationShape; + private _authenticationProviders: Map = new Map(); + + private _onDidRegisterAuthenticationProvider = new Emitter(); + readonly onDidRegisterAuthenticationProvider: Event = this._onDidRegisterAuthenticationProvider.event; + + readonly onDidUnregisterAuthenticationProvider: Event = _onDidUnregisterAuthenticationProvider.event; + + readonly onDidChangeSessions: Event = _onDidChangeSessions.event; + + constructor(mainContext: IMainContext) { + this._proxy = mainContext.getProxy(MainContext.MainThreadAuthentication); + + this.onDidUnregisterAuthenticationProvider(providerId => { + this._authenticationProviders.delete(providerId); + }); + } + + registerAuthenticationProvider(provider: vscode.AuthenticationProvider) { + if (this._authenticationProviders.get(provider.id)) { + throw new Error(`An authentication provider with id '${provider.id}' is already registered.`); + } + + const authenticationProvider = new ExtHostAuthenticationProvider(provider, provider.id, this._proxy); + this._authenticationProviders.set(provider.id, authenticationProvider); + + this._proxy.$registerAuthenticationProvider(provider.id); + this._onDidRegisterAuthenticationProvider.fire(provider.id); + return authenticationProvider; + } + + $login(providerId: string): Promise { + const authProvider = this._authenticationProviders.get(providerId); + if (authProvider) { + return Promise.resolve(authProvider.login()); + } + + throw new Error(`Unable to find authentication provider with handle: ${0}`); + } + + $logout(providerId: string, sessionId: string): Promise { + const authProvider = this._authenticationProviders.get(providerId); + if (authProvider) { + return Promise.resolve(authProvider.logout(sessionId)); + } + + throw new Error(`Unable to find authentication provider with handle: ${0}`); + } + + $getSessions(providerId: string): Promise> { + const authProvider = this._authenticationProviders.get(providerId); + if (authProvider) { + return Promise.resolve(authProvider.getSessions()); + } + + throw new Error(`Unable to find authentication provider with handle: ${0}`); + } +} diff --git a/src/vs/workbench/api/common/extHostClipboard.ts b/src/vs/workbench/api/common/extHostClipboard.ts index 51a4a54c16..49f9423270 100644 --- a/src/vs/workbench/api/common/extHostClipboard.ts +++ b/src/vs/workbench/api/common/extHostClipboard.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IMainContext, MainContext, MainThreadClipboardShape } from 'vs/workbench/api/common/extHost.protocol'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; export class ExtHostClipboard implements vscode.Clipboard { diff --git a/src/vs/workbench/api/common/extHostCodeInsets.ts b/src/vs/workbench/api/common/extHostCodeInsets.ts index 2e289fee40..a452e93a7c 100644 --- a/src/vs/workbench/api/common/extHostCodeInsets.ts +++ b/src/vs/workbench/api/common/extHostCodeInsets.ts @@ -8,7 +8,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ExtHostTextEditor } from 'vs/workbench/api/common/extHostTextEditor'; import { ExtHostEditors } from 'vs/workbench/api/common/extHostTextEditors'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { ExtHostEditorInsetsShape, MainThreadEditorInsetsShape } from './extHost.protocol'; import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; import { generateUuid } from 'vs/base/common/uuid'; diff --git a/src/vs/workbench/api/common/extHostCommands.ts b/src/vs/workbench/api/common/extHostCommands.ts index ade390377f..dbed2f7dfc 100644 --- a/src/vs/workbench/api/common/extHostCommands.ts +++ b/src/vs/workbench/api/common/extHostCommands.ts @@ -11,7 +11,7 @@ import { cloneAndChange } from 'vs/base/common/objects'; import { MainContext, MainThreadCommandsShape, ExtHostCommandsShape, ObjectIdentifier, ICommandDto } from './extHost.protocol'; import { isNonEmptyArray } from 'vs/base/common/arrays'; import * as modes from 'vs/editor/common/modes'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { ILogService } from 'vs/platform/log/common/log'; import { revive } from 'vs/base/common/marshalling'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/workbench/api/common/extHostComments.ts b/src/vs/workbench/api/common/extHostComments.ts index 28b6fb716c..47a4cca2ae 100644 --- a/src/vs/workbench/api/common/extHostComments.ts +++ b/src/vs/workbench/api/common/extHostComments.ts @@ -15,7 +15,7 @@ import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensio import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import * as extHostTypeConverter from 'vs/workbench/api/common/extHostTypeConverters'; import * as types from 'vs/workbench/api/common/extHostTypes'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { ExtHostCommentsShape, IMainContext, MainContext, MainThreadCommentsShape, CommentThreadChanges } from './extHost.protocol'; import { ExtHostCommands } from './extHostCommands'; diff --git a/src/vs/workbench/api/common/extHostConfiguration.ts b/src/vs/workbench/api/common/extHostConfiguration.ts index f53736bd10..845584a70a 100644 --- a/src/vs/workbench/api/common/extHostConfiguration.ts +++ b/src/vs/workbench/api/common/extHostConfiguration.ts @@ -5,7 +5,7 @@ import { mixin, deepClone } from 'vs/base/common/objects'; import { Event, Emitter } from 'vs/base/common/event'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { ExtHostWorkspace, IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { ExtHostConfigurationShape, MainThreadConfigurationShape, IConfigurationInitData, MainContext } from './extHost.protocol'; import { ConfigurationTarget as ExtHostConfigurationTarget } from './extHostTypes'; @@ -45,12 +45,24 @@ type ConfigurationInspect = { userLanguageValue?: T; workspaceLanguageValue?: T; workspaceFolderLanguageValue?: T; + + languages?: string[]; }; -function isTextDocument(thing: any): thing is vscode.TextDocument { +function isUri(thing: any): thing is vscode.Uri { + return thing instanceof URI; +} + +function isResourceLanguage(thing: any): thing is { uri: URI, languageId: string } { return thing && thing.uri instanceof URI - && (!thing.languageId || typeof thing.languageId === 'string'); + && (thing.languageId && typeof thing.languageId === 'string'); +} + +function isLanguage(thing: any): thing is { languageId: string } { + return thing + && !thing.uri + && (thing.languageId && typeof thing.languageId === 'string'); } function isWorkspaceFolder(thing: any): thing is vscode.WorkspaceFolder { @@ -60,28 +72,18 @@ function isWorkspaceFolder(thing: any): thing is vscode.WorkspaceFolder { && (!thing.index || typeof thing.index === 'number'); } -function isUri(thing: any): thing is vscode.Uri { - return thing instanceof URI; -} - -function isResourceLanguage(thing: any): thing is { resource: URI, languageId: string } { - return thing - && thing.resource instanceof URI - && (!thing.languageId || typeof thing.languageId === 'string'); -} - function scopeToOverrides(scope: vscode.ConfigurationScope | undefined | null): IConfigurationOverrides | undefined { if (isUri(scope)) { return { resource: scope }; } - if (isWorkspaceFolder(scope)) { - return { resource: scope.uri }; - } - if (isTextDocument(scope)) { + if (isResourceLanguage(scope)) { return { resource: scope.uri, overrideIdentifier: scope.languageId }; } - if (isResourceLanguage(scope)) { - return scope; + if (isLanguage(scope)) { + return { overrideIdentifier: scope.languageId }; + } + if (isWorkspaceFolder(scope)) { + return { resource: scope.uri }; } return undefined; } @@ -264,6 +266,8 @@ export class ExtHostConfigProvider { userLanguageValue: config.user?.override, workspaceLanguageValue: config.workspace?.override, workspaceFolderLanguageValue: config.workspaceFolder?.override, + + languages: config.overrideIdentifiers }; } return undefined; diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index 981c641e76..c529a1ec03 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -27,7 +27,7 @@ import { IExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import { ISignService } from 'vs/platform/sign/common/sign'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { withNullAsUndefined } from 'vs/base/common/types'; @@ -315,6 +315,7 @@ export class ExtHostDebugServiceBase implements IExtHostDebugService, ExtHostDeb this._debugServiceProxy.$registerDebugConfigurationProvider(type, !!provider.provideDebugConfigurations, !!provider.resolveDebugConfiguration, + !!provider.resolveDebugConfigurationWithSubstitutedVariables, !!provider.debugAdapterExecutable, // TODO@AW: deprecated handle); @@ -336,7 +337,7 @@ export class ExtHostDebugServiceBase implements IExtHostDebugService, ExtHostDeb } // make sure that only one factory for this type is registered - if (this.getAdapterFactoryByType(type)) { + if (this.getAdapterDescriptorFactoryByType(type)) { throw new Error(`a DebugAdapterDescriptorFactory can only be registered once per a type.`); } @@ -411,89 +412,92 @@ export class ExtHostDebugServiceBase implements IExtHostDebugService, ExtHostDeb const session = await this.getSession(sessionDto); - return this.getAdapterDescriptor(this.getAdapterFactoryByType(session.type), session).then(daDescriptor => { + return this.getAdapterDescriptor(this.getAdapterDescriptorFactoryByType(session.type), session).then(daDescriptor => { - const adapter = this.convertToDto(daDescriptor); + if (!daDescriptor) { + throw new Error(`Couldn't find a debug adapter descriptor for debug type '${session.type}' (extension might have failed to activate)`); + } - const da: AbstractDebugAdapter | undefined = this.createDebugAdapter(adapter, session); + const adapterDescriptor = this.convertToDto(daDescriptor); + + const da = this.createDebugAdapter(adapterDescriptor, session); + if (!da) { + throw new Error(`Couldn't create a debug adapter for type '${session.type}'.`); + } const debugAdapter = da; - if (debugAdapter) { - this._debugAdapters.set(debugAdapterHandle, debugAdapter); + this._debugAdapters.set(debugAdapterHandle, debugAdapter); - return this.getDebugAdapterTrackers(session).then(tracker => { + return this.getDebugAdapterTrackers(session).then(tracker => { - if (tracker) { - this._debugAdaptersTrackers.set(debugAdapterHandle, tracker); - } + if (tracker) { + this._debugAdaptersTrackers.set(debugAdapterHandle, tracker); + } - debugAdapter.onMessage(async message => { + debugAdapter.onMessage(async message => { - if (message.type === 'request' && (message).command === 'handshake') { + if (message.type === 'request' && (message).command === 'handshake') { - const request = message; + const request = message; - const response: DebugProtocol.Response = { - type: 'response', - seq: 0, - command: request.command, - request_seq: request.seq, - success: true - }; + const response: DebugProtocol.Response = { + type: 'response', + seq: 0, + command: request.command, + request_seq: request.seq, + success: true + }; - if (!this._signService) { - this._signService = this.createSignService(); - } + if (!this._signService) { + this._signService = this.createSignService(); + } - try { - if (this._signService) { - const signature = await this._signService.sign(request.arguments.value); - response.body = { - signature: signature - }; - debugAdapter.sendResponse(response); - } else { - throw new Error('no signer'); - } - } catch (e) { - response.success = false; - response.message = e.message; + try { + if (this._signService) { + const signature = await this._signService.sign(request.arguments.value); + response.body = { + signature: signature + }; debugAdapter.sendResponse(response); + } else { + throw new Error('no signer'); } - } else { - if (tracker && tracker.onDidSendMessage) { - tracker.onDidSendMessage(message); - } - - // DA -> VS Code - message = convertToVSCPaths(message, true); - - mythis._debugServiceProxy.$acceptDAMessage(debugAdapterHandle, message); + } catch (e) { + response.success = false; + response.message = e.message; + debugAdapter.sendResponse(response); } - }); - debugAdapter.onError(err => { - if (tracker && tracker.onError) { - tracker.onError(err); + } else { + if (tracker && tracker.onDidSendMessage) { + tracker.onDidSendMessage(message); } - this._debugServiceProxy.$acceptDAError(debugAdapterHandle, err.name, err.message, err.stack); - }); - debugAdapter.onExit((code: number | null) => { - if (tracker && tracker.onExit) { - tracker.onExit(withNullAsUndefined(code), undefined); - } - this._debugServiceProxy.$acceptDAExit(debugAdapterHandle, withNullAsUndefined(code), undefined); - }); - if (tracker && tracker.onWillStartSession) { - tracker.onWillStartSession(); + // DA -> VS Code + message = convertToVSCPaths(message, true); + + mythis._debugServiceProxy.$acceptDAMessage(debugAdapterHandle, message); } - - return debugAdapter.startSession(); + }); + debugAdapter.onError(err => { + if (tracker && tracker.onError) { + tracker.onError(err); + } + this._debugServiceProxy.$acceptDAError(debugAdapterHandle, err.name, err.message, err.stack); + }); + debugAdapter.onExit((code: number | null) => { + if (tracker && tracker.onExit) { + tracker.onExit(withNullAsUndefined(code), undefined); + } + this._debugServiceProxy.$acceptDAExit(debugAdapterHandle, withNullAsUndefined(code), undefined); }); - } - return undefined; + if (tracker && tracker.onWillStartSession) { + tracker.onWillStartSession(); + } + + return debugAdapter.startSession(); + }); }); } @@ -628,6 +632,20 @@ export class ExtHostDebugServiceBase implements IExtHostDebugService, ExtHostDeb }); } + public $resolveDebugConfigurationWithSubstitutedVariables(configProviderHandle: number, folderUri: UriComponents | undefined, debugConfiguration: vscode.DebugConfiguration, token: CancellationToken): Promise { + return asPromise(async () => { + const provider = this.getConfigProviderByHandle(configProviderHandle); + if (!provider) { + throw new Error('no DebugConfigurationProvider found'); + } + if (!provider.resolveDebugConfigurationWithSubstitutedVariables) { + throw new Error('DebugConfigurationProvider has no method resolveDebugConfigurationWithSubstitutedVariables'); + } + const folder = await this.getFolder(folderUri); + return provider.resolveDebugConfigurationWithSubstitutedVariables(folder, debugConfiguration, token); + }); + } + // TODO@AW deprecated and legacy public $legacyDebugAdapterExecutable(configProviderHandle: number, folderUri: UriComponents | undefined): Promise { return asPromise(async () => { @@ -648,13 +666,18 @@ export class ExtHostDebugServiceBase implements IExtHostDebugService, ExtHostDeb }); } - public async $provideDebugAdapter(adapterProviderHandle: number, sessionDto: IDebugSessionDto): Promise { - const adapterProvider = this.getAdapterProviderByHandle(adapterProviderHandle); - if (!adapterProvider) { - return Promise.reject(new Error('no handler found')); + public async $provideDebugAdapter(adapterFactoryHandle: number, sessionDto: IDebugSessionDto): Promise { + const adapterDescriptorFactory = this.getAdapterDescriptorFactoryByHandle(adapterFactoryHandle); + if (!adapterDescriptorFactory) { + return Promise.reject(new Error('no adapter descriptor factory found for handle')); } const session = await this.getSession(sessionDto); - return this.getAdapterDescriptor(adapterProvider, session).then(x => this.convertToDto(x)); + return this.getAdapterDescriptor(adapterDescriptorFactory, session).then(adapterDescriptor => { + if (!adapterDescriptor) { + throw new Error(`Couldn't find a debug adapter descriptor for debug type '${session.type}'`); + } + return this.convertToDto(adapterDescriptor); + }); } public async $acceptDebugSessionStarted(sessionDto: IDebugSessionDto): Promise { @@ -694,7 +717,7 @@ export class ExtHostDebugServiceBase implements IExtHostDebugService, ExtHostDeb // private & dto helpers - private convertToDto(x: vscode.DebugAdapterDescriptor | undefined): IAdapterDescriptor { + private convertToDto(x: vscode.DebugAdapterDescriptor): IAdapterDescriptor { if (x instanceof DebugAdapterExecutable) { return { @@ -719,7 +742,7 @@ export class ExtHostDebugServiceBase implements IExtHostDebugService, ExtHostDeb } } - private getAdapterFactoryByType(type: string): vscode.DebugAdapterDescriptorFactory | undefined { + private getAdapterDescriptorFactoryByType(type: string): vscode.DebugAdapterDescriptorFactory | undefined { const results = this._adapterFactories.filter(p => p.type === type); if (results.length > 0) { return results[0].factory; @@ -727,7 +750,7 @@ export class ExtHostDebugServiceBase implements IExtHostDebugService, ExtHostDeb return undefined; } - private getAdapterProviderByHandle(handle: number): vscode.DebugAdapterDescriptorFactory | undefined { + private getAdapterDescriptorFactoryByHandle(handle: number): vscode.DebugAdapterDescriptorFactory | undefined { const results = this._adapterFactories.filter(p => p.handle === handle); if (results.length > 0) { return results[0].factory; @@ -789,7 +812,7 @@ export class ExtHostDebugServiceBase implements IExtHostDebugService, ExtHostDeb }); } - private async getAdapterDescriptor(adapterProvider: vscode.DebugAdapterDescriptorFactory | undefined, session: ExtHostDebugSession): Promise { + private async getAdapterDescriptor(adapterDescriptorFactory: vscode.DebugAdapterDescriptorFactory | undefined, session: ExtHostDebugSession): Promise { // a "debugServer" attribute in the launch config takes precedence const serverPort = session.configuration.debugServer; @@ -809,9 +832,9 @@ export class ExtHostDebugServiceBase implements IExtHostDebugService, ExtHostDeb }); } - if (adapterProvider) { + if (adapterDescriptorFactory) { const extensionRegistry = await this._extensionService.getExtensionRegistry(); - return asPromise(() => adapterProvider.createDebugAdapterDescriptor(session, this.daExecutableFromPackage(session, extensionRegistry))).then(daDescriptor => { + return asPromise(() => adapterDescriptorFactory.createDebugAdapterDescriptor(session, this.daExecutableFromPackage(session, extensionRegistry))).then(daDescriptor => { if (daDescriptor) { return daDescriptor; } diff --git a/src/vs/workbench/api/common/extHostDecorations.ts b/src/vs/workbench/api/common/extHostDecorations.ts index 6317bbe84e..fb9e982ad2 100644 --- a/src/vs/workbench/api/common/extHostDecorations.ts +++ b/src/vs/workbench/api/common/extHostDecorations.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { URI } from 'vs/base/common/uri'; import { MainContext, ExtHostDecorationsShape, MainThreadDecorationsShape, DecorationData, DecorationRequest, DecorationReply } from 'vs/workbench/api/common/extHost.protocol'; import { Disposable, Decoration } from 'vs/workbench/api/common/extHostTypes'; diff --git a/src/vs/workbench/api/common/extHostDiagnostics.ts b/src/vs/workbench/api/common/extHostDiagnostics.ts index 3c9292d5e2..fefd2abacd 100644 --- a/src/vs/workbench/api/common/extHostDiagnostics.ts +++ b/src/vs/workbench/api/common/extHostDiagnostics.ts @@ -6,7 +6,7 @@ import { localize } from 'vs/nls'; import { IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers'; import { URI, UriComponents } from 'vs/base/common/uri'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { MainContext, MainThreadDiagnosticsShape, ExtHostDiagnosticsShape, IMainContext } from './extHost.protocol'; import { DiagnosticSeverity } from './extHostTypes'; import * as converter from './extHostTypeConverters'; diff --git a/src/vs/workbench/api/common/extHostDialogs.ts b/src/vs/workbench/api/common/extHostDialogs.ts index c3221b1672..45dc19a9c6 100644 --- a/src/vs/workbench/api/common/extHostDialogs.ts +++ b/src/vs/workbench/api/common/extHostDialogs.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { URI } from 'vs/base/common/uri'; import { MainContext, MainThreadDiaglogsShape, IMainContext } from 'vs/workbench/api/common/extHost.protocol'; diff --git a/src/vs/workbench/api/common/extHostDocumentContentProviders.ts b/src/vs/workbench/api/common/extHostDocumentContentProviders.ts index 2377e139f6..b1d0974aae 100644 --- a/src/vs/workbench/api/common/extHostDocumentContentProviders.ts +++ b/src/vs/workbench/api/common/extHostDocumentContentProviders.ts @@ -7,7 +7,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Disposable } from 'vs/workbench/api/common/extHostTypes'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { MainContext, ExtHostDocumentContentProvidersShape, MainThreadDocumentContentProvidersShape, IMainContext } from './extHost.protocol'; import { ExtHostDocumentsAndEditors } from './extHostDocumentsAndEditors'; import { Schemas } from 'vs/base/common/network'; diff --git a/src/vs/workbench/api/common/extHostDocumentData.ts b/src/vs/workbench/api/common/extHostDocumentData.ts index fb883e475a..da6593cc99 100644 --- a/src/vs/workbench/api/common/extHostDocumentData.ts +++ b/src/vs/workbench/api/common/extHostDocumentData.ts @@ -11,13 +11,18 @@ import { MirrorTextModel } from 'vs/editor/common/model/mirrorTextModel'; import { ensureValidWordDefinition, getWordAtText } from 'vs/editor/common/model/wordHelper'; import { MainThreadDocumentsShape } from 'vs/workbench/api/common/extHost.protocol'; import { EndOfLine, Position, Range } from 'vs/workbench/api/common/extHostTypes'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { equals } from 'vs/base/common/arrays'; const _modeId2WordDefinition = new Map(); export function setWordDefinitionFor(modeId: string, wordDefinition: RegExp | undefined): void { - _modeId2WordDefinition.set(modeId, wordDefinition); + if (!wordDefinition) { + _modeId2WordDefinition.delete(modeId); + } else { + _modeId2WordDefinition.set(modeId, wordDefinition); + } } + export function getWordDefinitionFor(modeId: string): RegExp | undefined { return _modeId2WordDefinition.get(modeId); } diff --git a/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts b/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts index f9af051dae..80138b1354 100644 --- a/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts +++ b/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts @@ -12,7 +12,7 @@ import { TextEdit } from 'vs/workbench/api/common/extHostTypes'; import { Range, TextDocumentSaveReason, EndOfLine } from 'vs/workbench/api/common/extHostTypeConverters'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { SaveReason } from 'vs/workbench/common/editor'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { LinkedList } from 'vs/base/common/linkedList'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; diff --git a/src/vs/workbench/api/common/extHostDocuments.ts b/src/vs/workbench/api/common/extHostDocuments.ts index 164b4d446c..326639fafc 100644 --- a/src/vs/workbench/api/common/extHostDocuments.ts +++ b/src/vs/workbench/api/common/extHostDocuments.ts @@ -11,8 +11,9 @@ import { ExtHostDocumentsShape, IMainContext, MainContext, MainThreadDocumentsSh import { ExtHostDocumentData, setWordDefinitionFor } from 'vs/workbench/api/common/extHostDocumentData'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import * as TypeConverters from 'vs/workbench/api/common/extHostTypeConverters'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { assertIsDefined } from 'vs/base/common/types'; +import { deepFreeze } from 'vs/base/common/objects'; export class ExtHostDocuments implements ExtHostDocumentsShape { @@ -144,7 +145,7 @@ export class ExtHostDocuments implements ExtHostDocumentsShape { } data._acceptIsDirty(isDirty); data.onEvents(events); - this._onDidChangeDocument.fire({ + this._onDidChangeDocument.fire(deepFreeze({ document: data.document, contentChanges: events.changes.map((change) => { return { @@ -154,7 +155,7 @@ export class ExtHostDocuments implements ExtHostDocumentsShape { text: change.text }; }) - }); + })); } public setWordDefinitionFor(modeId: string, wordDefinition: RegExp | undefined): void { diff --git a/src/vs/workbench/api/common/extHostExtensionActivator.ts b/src/vs/workbench/api/common/extHostExtensionActivator.ts index ff00ea50a5..fc885c0ed2 100644 --- a/src/vs/workbench/api/common/extHostExtensionActivator.ts +++ b/src/vs/workbench/api/common/extHostExtensionActivator.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { IDisposable } from 'vs/base/common/lifecycle'; import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 6176711177..df112c18cf 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -20,7 +20,7 @@ import { ExtensionActivationError } from 'vs/workbench/services/extensions/commo import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import * as errors from 'vs/base/common/errors'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { Schemas } from 'vs/base/common/network'; import { VSBuffer } from 'vs/base/common/buffer'; @@ -662,7 +662,7 @@ export abstract class AbstractExtHostExtensionService implements ExtHostExtensio value: { authority, options, - tunnelInformation: { environmentTunnels: result.environmentTunnels } + tunnelInformation: { environmentTunnels: result.environmentTunnels, hideCandidatePorts: result.hideCandidatePorts } } }; } catch (err) { diff --git a/src/vs/workbench/api/common/extHostFileSystem.ts b/src/vs/workbench/api/common/extHostFileSystem.ts index 6af3598b40..dfbf69141b 100644 --- a/src/vs/workbench/api/common/extHostFileSystem.ts +++ b/src/vs/workbench/api/common/extHostFileSystem.ts @@ -5,7 +5,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { MainContext, IMainContext, ExtHostFileSystemShape, MainThreadFileSystemShape, IFileChangeDto } from './extHost.protocol'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import * as files from 'vs/platform/files/common/files'; import { IDisposable, toDisposable, dispose } from 'vs/base/common/lifecycle'; import { FileChangeType, FileSystemError } from 'vs/workbench/api/common/extHostTypes'; diff --git a/src/vs/workbench/api/common/extHostFileSystemEventService.ts b/src/vs/workbench/api/common/extHostFileSystemEventService.ts index 898d84ac90..8d93c7f392 100644 --- a/src/vs/workbench/api/common/extHostFileSystemEventService.ts +++ b/src/vs/workbench/api/common/extHostFileSystemEventService.ts @@ -7,7 +7,7 @@ import { AsyncEmitter, Emitter, Event, IWaitUntil } from 'vs/base/common/event'; import { IRelativePattern, parse } from 'vs/base/common/glob'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, MainContext, MainThreadTextEditorsShape, IResourceFileEditDto, IResourceTextEditDto } from './extHost.protocol'; import * as typeConverter from './extHostTypeConverters'; import { Disposable, WorkspaceEdit } from './extHostTypes'; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 7e6a02b4f0..0b6b30e9c9 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -5,7 +5,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { mixin } from 'vs/base/common/objects'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; import { Range, Disposable, CompletionList, SnippetString, CodeActionKind, SymbolInformation, DocumentSymbol, SemanticTokensEdits } from 'vs/workbench/api/common/extHostTypes'; import { ISingleEditOperation } from 'vs/editor/common/model'; @@ -29,6 +29,7 @@ import { DisposableStore, dispose } from 'vs/base/common/lifecycle'; import { VSBuffer } from 'vs/base/common/buffer'; import { encodeSemanticTokensDto } from 'vs/workbench/api/common/shared/semanticTokens'; import { IdGenerator } from 'vs/base/common/idGenerator'; +import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; // --- adapter @@ -326,7 +327,8 @@ class CodeActionAdapter { private readonly _diagnostics: ExtHostDiagnostics, private readonly _provider: vscode.CodeActionProvider, private readonly _logService: ILogService, - private readonly _extensionId: ExtensionIdentifier + private readonly _extension: IExtensionDescription, + private readonly _apiDeprecation: IExtHostApiDeprecationService, ) { } provideCodeActions(resource: URI, rangeOrSelection: IRange | ISelection, context: modes.CodeActionContext, token: CancellationToken): Promise { @@ -367,6 +369,10 @@ class CodeActionAdapter { } if (CodeActionAdapter._isCommand(candidate)) { // old school: synthetic code action + + this._apiDeprecation.report('CodeActionProvider.provideCodeActions - return commands', this._extension, + `Return 'CodeAction' instances instead.`); + actions.push({ _isSynthetic: true, title: candidate.title, @@ -375,9 +381,9 @@ class CodeActionAdapter { } else { if (codeActionContext.only) { if (!candidate.kind) { - this._logService.warn(`${this._extensionId.value} - Code actions of kind '${codeActionContext.only.value} 'requested but returned code action does not have a 'kind'. Code action will be dropped. Please set 'CodeAction.kind'.`); + this._logService.warn(`${this._extension.identifier.value} - Code actions of kind '${codeActionContext.only.value} 'requested but returned code action does not have a 'kind'. Code action will be dropped. Please set 'CodeAction.kind'.`); } else if (!codeActionContext.only.contains(candidate.kind)) { - this._logService.warn(`${this._extensionId.value} - Code actions of kind '${codeActionContext.only.value} 'requested but returned code action is of kind '${candidate.kind.value}'. Code action will be dropped. Please check 'CodeActionContext.only' to only return requested code actions.`); + this._logService.warn(`${this._extension.identifier.value} - Code actions of kind '${codeActionContext.only.value} 'requested but returned code action is of kind '${candidate.kind.value}'. Code action will be dropped. Please check 'CodeActionContext.only' to only return requested code actions.`); } } @@ -623,37 +629,38 @@ class SemanticTokensPreviousResult { ) { } } -export class SemanticTokensAdapter { +export class DocumentSemanticTokensAdapter { private readonly _previousResults: Map; private _nextResultId = 1; constructor( private readonly _documents: ExtHostDocuments, - private readonly _provider: vscode.SemanticTokensProvider, + private readonly _provider: vscode.DocumentSemanticTokensProvider, ) { this._previousResults = new Map(); } - provideSemanticTokens(resource: URI, ranges: IRange[] | null, previousResultId: number, token: CancellationToken): Promise { + provideDocumentSemanticTokens(resource: URI, previousResultId: number, token: CancellationToken): Promise { const doc = this._documents.getDocument(resource); const previousResult = (previousResultId !== 0 ? this._previousResults.get(previousResultId) : null); - const opts: vscode.SemanticTokensRequestOptions = { - ranges: (Array.isArray(ranges) && ranges.length > 0 ? ranges.map(typeConvert.Range.to) : undefined), - previousResultId: (previousResult ? previousResult.resultId : undefined) - }; - return asPromise(() => this._provider.provideSemanticTokens(doc, opts, token)).then(value => { - if (!value) { - return null; + return asPromise(() => { + if (previousResult && typeof previousResult.resultId === 'string' && typeof this._provider.provideDocumentSemanticTokensEdits === 'function') { + return this._provider.provideDocumentSemanticTokensEdits(doc, previousResult.resultId, token); } + return this._provider.provideDocumentSemanticTokens(doc, token); + }).then(value => { if (previousResult) { this._previousResults.delete(previousResultId); } - return this._send(SemanticTokensAdapter._convertToEdits(previousResult, value), value); + if (!value) { + return null; + } + return this._send(DocumentSemanticTokensAdapter._convertToEdits(previousResult, value), value); }); } - async releaseSemanticColoring(semanticColoringResultId: number): Promise { + async releaseDocumentSemanticColoring(semanticColoringResultId: number): Promise { this._previousResults.delete(semanticColoringResultId); } @@ -666,7 +673,7 @@ export class SemanticTokensAdapter { } private static _convertToEdits(previousResult: SemanticTokensPreviousResult | null | undefined, newResult: vscode.SemanticTokens | vscode.SemanticTokensEdits): vscode.SemanticTokens | vscode.SemanticTokensEdits { - if (!SemanticTokensAdapter._isSemanticTokens(newResult)) { + if (!DocumentSemanticTokensAdapter._isSemanticTokens(newResult)) { return newResult; } if (!previousResult || !previousResult.tokens) { @@ -702,7 +709,7 @@ export class SemanticTokensAdapter { } private _send(value: vscode.SemanticTokens | vscode.SemanticTokensEdits, original: vscode.SemanticTokens | vscode.SemanticTokensEdits): VSBuffer | null { - if (SemanticTokensAdapter._isSemanticTokens(value)) { + if (DocumentSemanticTokensAdapter._isSemanticTokens(value)) { const myId = this._nextResultId++; this._previousResults.set(myId, new SemanticTokensPreviousResult(value.resultId, value.data)); return encodeSemanticTokensDto({ @@ -712,9 +719,9 @@ export class SemanticTokensAdapter { }); } - if (SemanticTokensAdapter._isSemanticTokensEdits(value)) { + if (DocumentSemanticTokensAdapter._isSemanticTokensEdits(value)) { const myId = this._nextResultId++; - if (SemanticTokensAdapter._isSemanticTokens(original)) { + if (DocumentSemanticTokensAdapter._isSemanticTokens(original)) { // store the original this._previousResults.set(myId, new SemanticTokensPreviousResult(original.resultId, original.data)); } else { @@ -731,6 +738,33 @@ export class SemanticTokensAdapter { } } +export class DocumentRangeSemanticTokensAdapter { + + constructor( + private readonly _documents: ExtHostDocuments, + private readonly _provider: vscode.DocumentRangeSemanticTokensProvider, + ) { + } + + provideDocumentRangeSemanticTokens(resource: URI, range: IRange, token: CancellationToken): Promise { + const doc = this._documents.getDocument(resource); + return asPromise(() => this._provider.provideDocumentRangeSemanticTokens(doc, typeConvert.Range.to(range), token)).then(value => { + if (!value) { + return null; + } + return this._send(value); + }); + } + + private _send(value: vscode.SemanticTokens): VSBuffer | null { + return encodeSemanticTokensDto({ + id: 0, + type: 'full', + data: value.data + }); + } +} + class SuggestAdapter { static supportsResolving(provider: vscode.CompletionItemProvider): boolean { @@ -748,8 +782,9 @@ class SuggestAdapter { private readonly _commands: CommandsConverter, private readonly _provider: vscode.CompletionItemProvider, private readonly _logService: ILogService, + private readonly _apiDeprecation: IExtHostApiDeprecationService, private readonly _telemetry: extHostProtocol.MainThreadTelemetryShape, - private readonly _extensionId: ExtensionIdentifier + private readonly _extension: IExtensionDescription, ) { } provideCompletionItems(resource: URI, position: IPosition, context: modes.CompletionContext, token: CancellationToken): Promise { @@ -787,7 +822,8 @@ class SuggestAdapter { x: pid, b: [], a: { replace: typeConvert.Range.from(replaceRange), insert: typeConvert.Range.from(insertRange) }, - c: list.isIncomplete || undefined + c: list.isIncomplete || undefined, + d: list.isDetailsResolved || undefined }; for (let i = 0; i < list.items.length; i++) { @@ -825,23 +861,27 @@ class SuggestAdapter { type BlameExtension = { extensionId: string; - kind: string + kind: string; + index: string; }; type BlameExtensionMeta = { extensionId: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' }; kind: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' }; + index: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' }; }; - if (!this._didWarnMust && _mustNotChange !== SuggestAdapter._mustNotChangeHash(resolvedItem)) { - this._logService.warn(`[${this._extensionId.value}] INVALID result from 'resolveCompletionItem', extension MUST NOT change any of: label, sortText, filterText, insertText, or textEdit`); - this._telemetry.$publicLog2('resolveCompletionItem/invalid', { extensionId: this._extensionId.value, kind: 'must' }); + let _mustNotChangeIndex = !this._didWarnMust && SuggestAdapter._mustNotChangeDiff(_mustNotChange, resolvedItem); + if (typeof _mustNotChangeIndex === 'string') { + this._logService.warn(`[${this._extension.identifier.value}] INVALID result from 'resolveCompletionItem', extension MUST NOT change any of: label, sortText, filterText, insertText, or textEdit`); + this._telemetry.$publicLog2('resolveCompletionItem/invalid', { extensionId: this._extension.identifier.value, kind: 'must', index: _mustNotChangeIndex }); this._didWarnMust = true; } - if (!this._didWarnShould && _mayNotChange !== SuggestAdapter._mayNotChangeHash(resolvedItem)) { - this._logService.info(`[${this._extensionId.value}] UNSAVE result from 'resolveCompletionItem', extension SHOULD NOT change any of: additionalTextEdits, or command`); - this._telemetry.$publicLog2('resolveCompletionItem/invalid', { extensionId: this._extensionId.value, kind: 'should' }); + let _mayNotChangeIndex = !this._didWarnShould && SuggestAdapter._mayNotChangeDiff(_mayNotChange, resolvedItem); + if (typeof _mayNotChangeIndex === 'string') { + this._logService.info(`[${this._extension.identifier.value}] UNSAVE result from 'resolveCompletionItem', extension SHOULD NOT change any of: additionalTextEdits, or command`); + this._telemetry.$publicLog2('resolveCompletionItem/invalid', { extensionId: this._extension.identifier.value, kind: 'should', index: _mayNotChangeIndex }); this._didWarnShould = true; } @@ -878,7 +918,7 @@ class SuggestAdapter { [extHostProtocol.ISuggestDataDtoField.documentation]: typeof item.documentation === 'undefined' ? undefined : typeConvert.MarkdownString.fromStrict(item.documentation), [extHostProtocol.ISuggestDataDtoField.sortText]: item.sortText, [extHostProtocol.ISuggestDataDtoField.filterText]: item.filterText, - [extHostProtocol.ISuggestDataDtoField.preselect]: item.preselect, + [extHostProtocol.ISuggestDataDtoField.preselect]: item.preselect || undefined, [extHostProtocol.ISuggestDataDtoField.insertTextRules]: item.keepWhitespace ? modes.CompletionItemInsertTextRule.KeepWhitespace : 0, [extHostProtocol.ISuggestDataDtoField.commitCharacters]: item.commitCharacters, [extHostProtocol.ISuggestDataDtoField.additionalTextEdits]: item.additionalTextEdits && item.additionalTextEdits.map(typeConvert.TextEdit.from), @@ -887,6 +927,9 @@ class SuggestAdapter { // 'insertText'-logic if (item.textEdit) { + this._apiDeprecation.report('CompletionItem.textEdit', this._extension, + `Use 'CompletionItem.insertText' and 'CompletionItem.range' instead.`); + result[extHostProtocol.ISuggestDataDtoField.insertText] = item.textEdit.newText; } else if (typeof item.insertText === 'string') { @@ -903,8 +946,6 @@ class SuggestAdapter { range = item.textEdit.range; } else if (item.range) { range = item.range; - } else if (item.range2) { - range = item.range2; } if (range) { @@ -940,14 +981,43 @@ class SuggestAdapter { } private static _mustNotChangeHash(item: vscode.CompletionItem) { - const args = [item.label, item.sortText, item.filterText, item.insertText, item.range, item.range2]; - const res = JSON.stringify(args); + const res = JSON.stringify([item.label, item.sortText, item.filterText, item.insertText, item.range]); return res; } + private static _mustNotChangeDiff(hash: string, item: vscode.CompletionItem): string | void { + const thisArr = [item.label, item.sortText, item.filterText, item.insertText, item.range]; + const thisHash = JSON.stringify(thisArr); + if (hash === thisHash) { + return; + } + const arr = JSON.parse(hash); + for (let i = 0; i < 6; i++) { + if (JSON.stringify(arr[i] !== JSON.stringify(thisArr[i]))) { + return i.toString(); + } + } + return 'unknown'; + } + private static _mayNotChangeHash(item: vscode.CompletionItem) { return JSON.stringify([item.additionalTextEdits, item.command]); } + + private static _mayNotChangeDiff(hash: string, item: vscode.CompletionItem): string | void { + const thisArr = [item.additionalTextEdits, item.command]; + const thisHash = JSON.stringify(thisArr); + if (hash === thisHash) { + return; + } + const arr = JSON.parse(hash); + for (let i = 0; i < 6; i++) { + if (JSON.stringify(arr[i] !== JSON.stringify(thisArr[i]))) { + return i.toString(); + } + } + return 'unknown'; + } } class SignatureHelpAdapter { @@ -1282,9 +1352,9 @@ class CallHierarchyAdapter { type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | HoverAdapter | DocumentHighlightAdapter | ReferenceAdapter | CodeActionAdapter | DocumentFormattingAdapter | RangeFormattingAdapter | OnTypeFormattingAdapter | NavigateTypeAdapter | RenameAdapter - | SemanticTokensAdapter | SuggestAdapter | SignatureHelpAdapter | LinkProviderAdapter - | ImplementationAdapter | TypeDefinitionAdapter | ColorProviderAdapter | FoldingProviderAdapter - | DeclarationAdapter | SelectionRangeAdapter | CallHierarchyAdapter; + | SuggestAdapter | SignatureHelpAdapter | LinkProviderAdapter | ImplementationAdapter + | TypeDefinitionAdapter | ColorProviderAdapter | FoldingProviderAdapter | DeclarationAdapter + | SelectionRangeAdapter | CallHierarchyAdapter | DocumentSemanticTokensAdapter | DocumentRangeSemanticTokensAdapter; class AdapterData { constructor( @@ -1305,6 +1375,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF private _diagnostics: ExtHostDiagnostics; private _adapter = new Map(); private readonly _logService: ILogService; + private readonly _apiDeprecation: IExtHostApiDeprecationService; constructor( mainContext: extHostProtocol.IMainContext, @@ -1312,7 +1383,8 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF documents: ExtHostDocuments, commands: ExtHostCommands, diagnostics: ExtHostDiagnostics, - logService: ILogService + logService: ILogService, + apiDeprecationService: IExtHostApiDeprecationService, ) { this._uriTransformer = uriTransformer; this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadLanguageFeatures); @@ -1321,6 +1393,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF this._commands = commands; this._diagnostics = diagnostics; this._logService = logService; + this._apiDeprecation = apiDeprecationService; } private _transformDocumentSelector(selector: vscode.DocumentSelector): Array { @@ -1528,7 +1601,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF // --- quick fix registerCodeActionProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.CodeActionProvider, metadata?: vscode.CodeActionProviderMetadata): vscode.Disposable { - const handle = this._addNewAdapter(new CodeActionAdapter(this._documents, this._commands.converter, this._diagnostics, provider, this._logService, extension.identifier), extension); + const handle = this._addNewAdapter(new CodeActionAdapter(this._documents, this._commands.converter, this._diagnostics, provider, this._logService, extension, this._apiDeprecation), extension); this._proxy.$registerQuickFixSupport(handle, this._transformDocumentSelector(selector), (metadata && metadata.providedCodeActionKinds) ? metadata.providedCodeActionKinds.map(kind => kind.value) : undefined); return this._createDisposable(handle); } @@ -1612,18 +1685,28 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF //#region semantic coloring - registerSemanticTokensProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.SemanticTokensProvider, legend: vscode.SemanticTokensLegend): vscode.Disposable { - const handle = this._addNewAdapter(new SemanticTokensAdapter(this._documents, provider), extension); - this._proxy.$registerSemanticTokensProvider(handle, this._transformDocumentSelector(selector), legend); + registerDocumentSemanticTokensProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.DocumentSemanticTokensProvider, legend: vscode.SemanticTokensLegend): vscode.Disposable { + const handle = this._addNewAdapter(new DocumentSemanticTokensAdapter(this._documents, provider), extension); + this._proxy.$registerDocumentSemanticTokensProvider(handle, this._transformDocumentSelector(selector), legend); return this._createDisposable(handle); } - $provideSemanticTokens(handle: number, resource: UriComponents, ranges: IRange[] | null, previousResultId: number, token: CancellationToken): Promise { - return this._withAdapter(handle, SemanticTokensAdapter, adapter => adapter.provideSemanticTokens(URI.revive(resource), ranges, previousResultId, token), null); + $provideDocumentSemanticTokens(handle: number, resource: UriComponents, previousResultId: number, token: CancellationToken): Promise { + return this._withAdapter(handle, DocumentSemanticTokensAdapter, adapter => adapter.provideDocumentSemanticTokens(URI.revive(resource), previousResultId, token), null); } - $releaseSemanticTokens(handle: number, semanticColoringResultId: number): void { - this._withAdapter(handle, SemanticTokensAdapter, adapter => adapter.releaseSemanticColoring(semanticColoringResultId), undefined); + $releaseDocumentSemanticTokens(handle: number, semanticColoringResultId: number): void { + this._withAdapter(handle, DocumentSemanticTokensAdapter, adapter => adapter.releaseDocumentSemanticColoring(semanticColoringResultId), undefined); + } + + registerDocumentRangeSemanticTokensProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.DocumentRangeSemanticTokensProvider, legend: vscode.SemanticTokensLegend): vscode.Disposable { + const handle = this._addNewAdapter(new DocumentRangeSemanticTokensAdapter(this._documents, provider), extension); + this._proxy.$registerDocumentRangeSemanticTokensProvider(handle, this._transformDocumentSelector(selector), legend); + return this._createDisposable(handle); + } + + $provideDocumentRangeSemanticTokens(handle: number, resource: UriComponents, range: IRange, token: CancellationToken): Promise { + return this._withAdapter(handle, DocumentRangeSemanticTokensAdapter, adapter => adapter.provideDocumentRangeSemanticTokens(URI.revive(resource), range, token), null); } //#endregion @@ -1631,7 +1714,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF // --- suggestion registerCompletionItemProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.CompletionItemProvider, triggerCharacters: string[]): vscode.Disposable { - const handle = this._addNewAdapter(new SuggestAdapter(this._documents, this._commands.converter, provider, this._logService, this._telemetryShape, extension.identifier), extension); + const handle = this._addNewAdapter(new SuggestAdapter(this._documents, this._commands.converter, provider, this._logService, this._apiDeprecation, this._telemetryShape, extension), extension); this._proxy.$registerSuggestSupport(handle, this._transformDocumentSelector(selector), triggerCharacters, SuggestAdapter.supportsResolving(provider), extension.identifier); return this._createDisposable(handle); } @@ -1779,7 +1862,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return onEnterRules.map(ExtHostLanguageFeatures._serializeOnEnterRule); } - setLanguageConfiguration(languageId: string, configuration: vscode.LanguageConfiguration): vscode.Disposable { + setLanguageConfiguration(extension: IExtensionDescription, languageId: string, configuration: vscode.LanguageConfiguration): vscode.Disposable { let { wordPattern } = configuration; // check for a valid word pattern @@ -1794,6 +1877,16 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF this._documents.setWordDefinitionFor(languageId, undefined); } + if (configuration.__electricCharacterSupport) { + this._apiDeprecation.report('LanguageConfiguration.__electricCharacterSupport', extension, + `Do not use.`); + } + + if (configuration.__characterPairSupport) { + this._apiDeprecation.report('LanguageConfiguration.__characterPairSupport', extension, + `Do not use.`); + } + const handle = this._nextHandle(); const serializedConfiguration: extHostProtocol.ILanguageConfigurationDto = { comments: configuration.comments, diff --git a/src/vs/workbench/api/common/extHostLanguages.ts b/src/vs/workbench/api/common/extHostLanguages.ts index 2016185902..a4eb1fb091 100644 --- a/src/vs/workbench/api/common/extHostLanguages.ts +++ b/src/vs/workbench/api/common/extHostLanguages.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { MainContext, MainThreadLanguagesShape, IMainContext } from './extHost.protocol'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; export class ExtHostLanguages { diff --git a/src/vs/workbench/api/common/extHostMemento.ts b/src/vs/workbench/api/common/extHostMemento.ts index 669b9d11f2..3b0843ea7a 100644 --- a/src/vs/workbench/api/common/extHostMemento.ts +++ b/src/vs/workbench/api/common/extHostMemento.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { IDisposable } from 'vs/base/common/lifecycle'; import { ExtHostStorage } from 'vs/workbench/api/common/extHostStorage'; diff --git a/src/vs/workbench/api/common/extHostMessageService.ts b/src/vs/workbench/api/common/extHostMessageService.ts index f28190fc82..745904bc90 100644 --- a/src/vs/workbench/api/common/extHostMessageService.ts +++ b/src/vs/workbench/api/common/extHostMessageService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import Severity from 'vs/base/common/severity'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { MainContext, MainThreadMessageServiceShape, MainThreadMessageOptions, IMainContext } from './extHost.protocol'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; diff --git a/src/vs/workbench/api/common/extHostOutput.ts b/src/vs/workbench/api/common/extHostOutput.ts index 22389c5020..b70cf98580 100644 --- a/src/vs/workbench/api/common/extHostOutput.ts +++ b/src/vs/workbench/api/common/extHostOutput.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { MainContext, MainThreadOutputServiceShape, ExtHostOutputServiceShape } from './extHost.protocol'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index 658c93c135..ba0b7ce620 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -12,7 +12,7 @@ import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { MainContext, MainThreadSCMShape, SCMRawResource, SCMRawResourceSplice, SCMRawResourceSplices, IMainContext, ExtHostSCMShape, ICommandDto } from './extHost.protocol'; import { sortedDiff, equals } from 'vs/base/common/arrays'; import { comparePaths } from 'vs/base/common/comparers'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { ISplice } from 'vs/base/common/sequence'; import { ILogService } from 'vs/platform/log/common/log'; import { CancellationToken } from 'vs/base/common/cancellation'; diff --git a/src/vs/workbench/api/common/extHostSearch.ts b/src/vs/workbench/api/common/extHostSearch.ts index 6543a942d5..8227adb790 100644 --- a/src/vs/workbench/api/common/extHostSearch.ts +++ b/src/vs/workbench/api/common/extHostSearch.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { ExtHostSearchShape, MainThreadSearchShape, MainContext } from '../common/extHost.protocol'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { FileSearchManager } from 'vs/workbench/services/search/common/fileSearchManager'; diff --git a/src/vs/workbench/api/common/extHostTask.ts b/src/vs/workbench/api/common/extHostTask.ts index e41b8ac5d8..9a346b9395 100644 --- a/src/vs/workbench/api/common/extHostTask.ts +++ b/src/vs/workbench/api/common/extHostTask.ts @@ -12,7 +12,7 @@ import { MainContext, MainThreadTaskShape, ExtHostTaskShape } from 'vs/workbench import * as types from 'vs/workbench/api/common/extHostTypes'; import { IExtHostWorkspaceProvider, IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import * as tasks from '../common/shared/tasks'; import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index f093e0444e..a32e8a22c6 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { Event, Emitter } from 'vs/base/common/event'; import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShape, IShellLaunchConfigDto, IShellDefinitionDto, IShellAndArgsDto, ITerminalDimensionsDto } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; @@ -32,6 +32,7 @@ export interface IExtHostTerminalService extends ExtHostTerminalServiceShape { createExtensionTerminal(options: vscode.ExtensionTerminalOptions): vscode.Terminal; attachPtyToTerminal(id: number, pty: vscode.Pseudoterminal): void; getDefaultShell(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string; + getDefaultShellArgs(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string[] | string; } export const IExtHostTerminalService = createDecorator('IExtHostTerminalService'); @@ -310,9 +311,8 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService ) { - this._bufferer = new TerminalDataBufferer(); - this._proxy = extHostRpc.getProxy(MainContext.MainThreadTerminalService); + this._bufferer = new TerminalDataBufferer(this._proxy.$sendProcessData); this._onDidWriteTerminalData = new Emitter({ onFirstListenerAdd: () => this._proxy.$startSendingDataEvents(), onLastListenerRemove: () => this._proxy.$stopSendingDataEvents() @@ -322,6 +322,7 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ public abstract createTerminal(name?: string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal; public abstract createTerminalFromOptions(options: vscode.TerminalOptions): vscode.Terminal; public abstract getDefaultShell(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string; + public abstract getDefaultShellArgs(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string[] | string; public abstract $spawnExtHostProcess(id: number, shellLaunchConfigDto: IShellLaunchConfigDto, activeWorkspaceRootUriComponents: UriComponents, cols: number, rows: number, isWorkspaceShellAllowed: boolean): Promise; public abstract $requestAvailableShells(): Promise; public abstract $requestDefaultShellAndArgs(useAutomationShell: boolean): Promise; @@ -477,7 +478,7 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ p.onProcessTitleChanged(title => this._proxy.$sendProcessTitle(id, title)); // Buffer data events to reduce the amount of messages going to the renderer - this._bufferer.startBuffering(id, p.onProcessData, this._proxy.$sendProcessData); + this._bufferer.startBuffering(id, p.onProcessData); p.onProcessExit(exitCode => this._onProcessExit(id, exitCode)); if (p.onProcessOverrideDimensions) { @@ -596,6 +597,10 @@ export class WorkerExtHostTerminalService extends BaseExtHostTerminalService { throw new Error('Not implemented'); } + public getDefaultShellArgs(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string[] | string { + throw new Error('Not implemented'); + } + public $spawnExtHostProcess(id: number, shellLaunchConfigDto: IShellLaunchConfigDto, activeWorkspaceRootUriComponents: UriComponents, cols: number, rows: number, isWorkspaceShellAllowed: boolean): Promise { throw new Error('Not implemented'); } diff --git a/src/vs/workbench/api/common/extHostTextEditor.ts b/src/vs/workbench/api/common/extHostTextEditor.ts index a1f3400a7b..6aa3787c9d 100644 --- a/src/vs/workbench/api/common/extHostTextEditor.ts +++ b/src/vs/workbench/api/common/extHostTextEditor.ts @@ -13,7 +13,7 @@ import { IResolvedTextEditorConfiguration, ITextEditorConfigurationUpdate, MainT import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData'; import * as TypeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { EndOfLine, Position, Range, Selection, SnippetString, TextEditorLineNumbersStyle, TextEditorRevealType } from 'vs/workbench/api/common/extHostTypes'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { ILogService } from 'vs/platform/log/common/log'; export class TextEditorDecorationType implements vscode.TextEditorDecorationType { diff --git a/src/vs/workbench/api/common/extHostTextEditors.ts b/src/vs/workbench/api/common/extHostTextEditors.ts index 85ea8fdd1a..7b7f0b437d 100644 --- a/src/vs/workbench/api/common/extHostTextEditors.ts +++ b/src/vs/workbench/api/common/extHostTextEditors.ts @@ -10,7 +10,7 @@ import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocum import { ExtHostTextEditor, TextEditorDecorationType } from 'vs/workbench/api/common/extHostTextEditor'; import * as TypeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { TextEditorSelectionChangeKind } from 'vs/workbench/api/common/extHostTypes'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; export class ExtHostEditors implements ExtHostEditorsShape { diff --git a/src/vs/workbench/api/common/extHostTreeViews.ts b/src/vs/workbench/api/common/extHostTreeViews.ts index 38992389a0..18d6ccfab6 100644 --- a/src/vs/workbench/api/common/extHostTreeViews.ts +++ b/src/vs/workbench/api/common/extHostTreeViews.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { basename } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { Emitter, Event } from 'vs/base/common/event'; diff --git a/src/vs/workbench/api/common/extHostTunnelService.ts b/src/vs/workbench/api/common/extHostTunnelService.ts index 8519b527f4..530dfdb8f1 100644 --- a/src/vs/workbench/api/common/extHostTunnelService.ts +++ b/src/vs/workbench/api/common/extHostTunnelService.ts @@ -5,7 +5,7 @@ import { ExtHostTunnelServiceShape } from 'vs/workbench/api/common/extHost.protocol'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { RemoteTunnel, TunnelOptions } from 'vs/platform/remote/common/tunnel'; import { IDisposable } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index e58d532d13..8437b5a032 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -10,7 +10,7 @@ import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; import { IDecorationOptions, IThemeDecorationRenderOptions, IDecorationRenderOptions, IContentDecorationRenderOptions } from 'vs/editor/common/editorCommon'; import { EndOfLineSequence, TrackedRangeStickiness } from 'vs/editor/common/model'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ProgressLocation as MainProgressLocation } from 'vs/platform/progress/common/progress'; import { SaveReason } from 'vs/workbench/common/editor'; @@ -30,6 +30,7 @@ import { cloneAndChange } from 'vs/base/common/objects'; import { LogLevel as _MainLogLevel } from 'vs/platform/log/common/log'; import { coalesce, isNonEmptyArray } from 'vs/base/common/arrays'; import { RenderLineNumbersType } from 'vs/editor/common/config/editorOptions'; +import { CommandsConverter } from 'vs/workbench/api/common/extHostCommands'; export interface PositionLike { line: number; @@ -830,7 +831,7 @@ export namespace CompletionItemKind { export namespace CompletionItem { - export function to(suggestion: modes.CompletionItem): types.CompletionItem { + export function to(suggestion: modes.CompletionItem, converter?: CommandsConverter): types.CompletionItem { const result = new types.CompletionItem(suggestion.label); result.insertText = suggestion.insertText; result.kind = CompletionItemKind.to(suggestion.kind); @@ -841,17 +842,26 @@ export namespace CompletionItem { result.filterText = suggestion.filterText; result.preselect = suggestion.preselect; result.commitCharacters = suggestion.commitCharacters; - result.range = editorRange.Range.isIRange(suggestion.range) ? Range.to(suggestion.range) : undefined; - result.range2 = editorRange.Range.isIRange(suggestion.range) ? undefined : { inserting: Range.to(suggestion.range.insert), replacing: Range.to(suggestion.range.replace) }; + + // range + if (editorRange.Range.isIRange(suggestion.range)) { + result.range = Range.to(suggestion.range); + } else if (typeof suggestion.range === 'object') { + result.range = { inserting: Range.to(suggestion.range.insert), replacing: Range.to(suggestion.range.replace) }; + } + result.keepWhitespace = typeof suggestion.insertTextRules === 'undefined' ? false : Boolean(suggestion.insertTextRules & modes.CompletionItemInsertTextRule.KeepWhitespace); - // 'inserText'-logic + // 'insertText'-logic if (typeof suggestion.insertTextRules !== 'undefined' && suggestion.insertTextRules & modes.CompletionItemInsertTextRule.InsertAsSnippet) { result.insertText = new types.SnippetString(suggestion.insertText); } else { result.insertText = suggestion.insertText; result.textEdit = result.range instanceof types.Range ? new types.TextEdit(result.range, result.insertText) : undefined; } - // TODO additionalEdits, command + if (suggestion.additionalTextEdits && suggestion.additionalTextEdits.length > 0) { + result.additionalTextEdits = suggestion.additionalTextEdits.map(e => TextEdit.to(e as modes.TextEdit)); + } + result.command = converter && suggestion.command ? converter.fromInternal(suggestion.command) : undefined; return result; } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 8633ac1ad6..206a116d56 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -11,7 +11,7 @@ import { values } from 'vs/base/common/map'; import { startsWith } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { FileSystemProviderErrorCode, markAsFileSystemProviderError } from 'vs/platform/files/common/files'; import { RemoteAuthorityResolverErrorCode } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { escapeCodicons } from 'vs/base/common/codicons'; @@ -1364,8 +1364,7 @@ export class CompletionItem implements vscode.CompletionItem { preselect?: boolean; insertText?: string | SnippetString; keepWhitespace?: boolean; - range?: Range; - range2?: Range | { inserting: Range; replacing: Range; }; + range?: Range | { inserting: Range; replacing: Range; }; commitCharacters?: string[]; textEdit?: TextEdit; additionalTextEdits?: TextEdit[]; @@ -1395,7 +1394,7 @@ export class CompletionItem implements vscode.CompletionItem { export class CompletionList { isIncomplete?: boolean; - + isDetailsResolved?: boolean; items: vscode.CompletionItem[]; constructor(items: vscode.CompletionItem[] = [], isIncomplete: boolean = false) { diff --git a/src/vs/workbench/api/common/extHostUrls.ts b/src/vs/workbench/api/common/extHostUrls.ts index bcd081c9d3..5ff9d39a21 100644 --- a/src/vs/workbench/api/common/extHostUrls.ts +++ b/src/vs/workbench/api/common/extHostUrls.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { MainContext, IMainContext, ExtHostUrlsShape, MainThreadUrlsShape } from './extHost.protocol'; import { URI, UriComponents } from 'vs/base/common/uri'; import { toDisposable } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index 444d9dffae..1100c295b8 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -14,7 +14,7 @@ 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 * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { ExtHostWebviewsShape, IMainContext, MainContext, MainThreadWebviewsShape, WebviewEditorCapabilities, WebviewPanelHandle, WebviewPanelViewStateData } from './extHost.protocol'; import { Disposable as VSCodeDisposable } from './extHostTypes'; diff --git a/src/vs/workbench/api/common/extHostWorkspace.ts b/src/vs/workbench/api/common/extHostWorkspace.ts index cc5d6519d7..bfee5c2517 100644 --- a/src/vs/workbench/api/common/extHostWorkspace.ts +++ b/src/vs/workbench/api/common/extHostWorkspace.ts @@ -25,7 +25,7 @@ import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { Range, RelativePattern } from 'vs/workbench/api/common/extHostTypes'; import { ITextQueryBuilderOptions } from 'vs/workbench/contrib/search/common/queryBuilder'; import { IRawFileMatch2, resultIsMatch } from 'vs/workbench/services/search/common/search'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { ExtHostWorkspaceShape, IWorkspaceData, MainContext, MainThreadMessageServiceShape, MainThreadWorkspaceShape } from './extHost.protocol'; export interface IExtHostWorkspaceProvider { diff --git a/src/vs/workbench/api/common/menusExtensionPoint.ts b/src/vs/workbench/api/common/menusExtensionPoint.ts index 4cc49a5031..0e2fe51dc3 100644 --- a/src/vs/workbench/api/common/menusExtensionPoint.ts +++ b/src/vs/workbench/api/common/menusExtensionPoint.ts @@ -56,6 +56,7 @@ namespace schema { case 'comments/commentThread/context': return MenuId.CommentThreadActions; case 'comments/comment/title': return MenuId.CommentTitle; case 'comments/comment/context': return MenuId.CommentActions; + case 'extension/context': return MenuId.ExtensionContext; } return undefined; @@ -214,6 +215,11 @@ namespace schema { type: 'array', items: menuItem }, + 'extension/context': { + description: localize('menus.extensionContext', "The extension context menu"), + type: 'array', + items: menuItem + }, } }; diff --git a/src/vs/workbench/api/common/shared/webview.ts b/src/vs/workbench/api/common/shared/webview.ts index da8d308328..74a4e1253d 100644 --- a/src/vs/workbench/api/common/shared/webview.ts +++ b/src/vs/workbench/api/common/shared/webview.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; export interface WebviewInitData { readonly isExtensionDevelopmentDebug: boolean; diff --git a/src/vs/workbench/api/node/extHost.services.ts b/src/vs/workbench/api/node/extHost.services.ts index e2d5fdf6c8..739b0ced96 100644 --- a/src/vs/workbench/api/node/extHost.services.ts +++ b/src/vs/workbench/api/node/extHost.services.ts @@ -28,9 +28,11 @@ import { ILogService } from 'vs/platform/log/common/log'; import { ExtHostLogService } from 'vs/workbench/api/node/extHostLogService'; import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService'; import { ExtHostTunnelService } from 'vs/workbench/api/node/extHostTunnelService'; +import { IExtHostApiDeprecationService, ExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; // register singleton services registerSingleton(ILogService, ExtHostLogService); +registerSingleton(IExtHostApiDeprecationService, ExtHostApiDeprecationService); registerSingleton(IExtHostOutputService, ExtHostOutputService2); registerSingleton(IExtHostWorkspace, ExtHostWorkspace); registerSingleton(IExtHostDecorations, ExtHostDecorations); diff --git a/src/vs/workbench/api/node/extHostCLIServer.ts b/src/vs/workbench/api/node/extHostCLIServer.ts index 43fd5516a3..8e8867e180 100644 --- a/src/vs/workbench/api/node/extHostCLIServer.ts +++ b/src/vs/workbench/api/node/extHostCLIServer.ts @@ -11,6 +11,7 @@ import { IWindowOpenable } from 'vs/platform/windows/common/windows'; import { URI } from 'vs/base/common/uri'; import { hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; import { INativeOpenWindowOptions } from 'vs/platform/windows/node/window'; +import { ILogService } from 'vs/platform/log/common/log'; export interface OpenCommandPipeArgs { type: 'open'; @@ -39,10 +40,10 @@ export class CLIServer { private _server: http.Server; private _ipcHandlePath: string | undefined; - constructor(@IExtHostCommands private _commands: IExtHostCommands) { + constructor(@IExtHostCommands private _commands: IExtHostCommands, @ILogService private logService: ILogService) { this._server = http.createServer((req, res) => this.onRequest(req, res)); this.setup().catch(err => { - console.error(err); + logService.error(err); return ''; }); } @@ -56,9 +57,9 @@ export class CLIServer { try { this._server.listen(this.ipcHandlePath); - this._server.on('error', err => console.error(err)); + this._server.on('error', err => this.logService.error(err)); } catch (err) { - console.error('Could not start open from terminal server.'); + this.logService.error('Could not start open from terminal server.'); } return this._ipcHandlePath; @@ -79,13 +80,13 @@ export class CLIServer { break; case 'command': this.runCommand(data, res) - .catch(console.error); + .catch(this.logService.error); break; default: res.writeHead(404); res.write(`Unknown message type: ${data.type}`, err => { if (err) { - console.error(err); + this.logService.error(err); } }); res.end(); @@ -139,7 +140,7 @@ export class CLIServer { res.writeHead(500); res.write(String(err), err => { if (err) { - console.error(err); + this.logService.error(err); } }); res.end(); @@ -153,7 +154,7 @@ export class CLIServer { res.writeHead(200); res.write(JSON.stringify(result), err => { if (err) { - console.error(err); + this.logService.error(err); } }); res.end(); @@ -161,7 +162,7 @@ export class CLIServer { res.writeHead(500); res.write(String(err), err => { if (err) { - console.error(err); + this.logService.error(err); } }); res.end(); diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index 29668be492..881e4bcea1 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; +import * as env from 'vs/base/common/platform'; import { DebugAdapterExecutable } from 'vs/workbench/api/common/extHostTypes'; import { ExecutableDebugAdapter, SocketDebugAdapter } from 'vs/workbench/contrib/debug/node/debugAdapter'; import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; @@ -23,7 +24,6 @@ import { SignService } from 'vs/platform/sign/node/signService'; import { hasChildProcesses, prepareCommand, runInExternalTerminal } from 'vs/workbench/contrib/debug/node/terminals'; import { IDisposable } from 'vs/base/common/lifecycle'; import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; -import { IProcessEnvironment } from 'vs/base/common/platform'; export class ExtHostDebugService extends ExtHostDebugServiceBase { @@ -108,7 +108,7 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { terminal.show(); const shellProcessId = await this._integratedTerminalInstance.processId; - const command = prepareCommand(args, shell, configProvider); + const command = prepareCommand(args, shell); terminal.sendText(command, true); return shellProcessId; @@ -121,7 +121,7 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { } protected createVariableResolver(folders: vscode.WorkspaceFolder[], editorService: ExtHostDocumentsAndEditors, configurationService: ExtHostConfigProvider): AbstractVariableResolverService { - return new ExtHostVariableResolverService(folders, editorService, configurationService, process.env as IProcessEnvironment); + return new ExtHostVariableResolverService(folders, editorService, configurationService, process.env as env.IProcessEnvironment); } } diff --git a/src/vs/workbench/api/node/extHostOutputService.ts b/src/vs/workbench/api/node/extHostOutputService.ts index 0fdc425eef..5285abeded 100644 --- a/src/vs/workbench/api/node/extHostOutputService.ts +++ b/src/vs/workbench/api/node/extHostOutputService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { MainThreadOutputServiceShape } from '../common/extHost.protocol'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { URI } from 'vs/base/common/uri'; import { join } from 'vs/base/common/path'; import { OutputAppender } from 'vs/workbench/services/output/node/outputAppender'; diff --git a/src/vs/workbench/api/node/extHostSearch.ts b/src/vs/workbench/api/node/extHostSearch.ts index cb9669501d..3c555d428d 100644 --- a/src/vs/workbench/api/node/extHostSearch.ts +++ b/src/vs/workbench/api/node/extHostSearch.ts @@ -11,7 +11,7 @@ import { IFileQuery, IRawFileQuery, ISearchCompleteStats, isSerializedFileMatch, import { SearchService } from 'vs/workbench/services/search/node/rawSearchService'; import { RipgrepSearchProvider } from 'vs/workbench/services/search/node/ripgrepSearchProvider'; import { OutputChannel } from 'vs/workbench/services/search/node/ripgrepSearchUtils'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { IURITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index 0e451c8706..036972ee9d 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -9,7 +9,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; // import { win32 } from 'vs/base/node/processes'; import * as types from 'vs/workbench/api/common/extHostTypes'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import * as tasks from '../common/shared/tasks'; import * as Objects from 'vs/base/common/objects'; import { ExtHostVariableResolverService } from 'vs/workbench/api/common/extHostDebugService'; diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index e2f96f5efe..5feaeaefb5 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import product from 'vs/platform/product/common/product'; import * as os from 'os'; import { URI, UriComponents } from 'vs/base/common/uri'; @@ -59,7 +59,7 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { } public getDefaultShell(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string { - const fetchSetting = (key: string) => { + const fetchSetting = (key: string): { userValue: string | string[] | undefined, value: string | string[] | undefined, defaultValue: string | string[] | undefined } => { const setting = configProvider .getConfiguration(key.substr(0, key.lastIndexOf('.'))) .inspect(key.substr(key.lastIndexOf('.') + 1)); @@ -78,8 +78,8 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { ); } - private _getDefaultShellArgs(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string[] | string { - const fetchSetting = (key: string) => { + public getDefaultShellArgs(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string[] | string { + const fetchSetting = (key: string): { userValue: string | string[] | undefined, value: string | string[] | undefined, defaultValue: string | string[] | undefined } => { const setting = configProvider .getConfiguration(key.substr(0, key.lastIndexOf('.'))) .inspect(key.substr(key.lastIndexOf('.') + 1)); @@ -91,11 +91,11 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { private _apiInspectConfigToPlain( config: { key: string; defaultValue?: T; globalValue?: T; workspaceValue?: T, workspaceFolderValue?: T } | undefined - ): { user: T | undefined, value: T | undefined, default: T | undefined } { + ): { userValue: T | undefined, value: T | undefined, defaultValue: T | undefined } { return { - user: config ? config.globalValue : undefined, + userValue: config ? config.globalValue : undefined, value: config ? config.workspaceValue : undefined, - default: config ? config.defaultValue : undefined, + defaultValue: config ? config.defaultValue : undefined, }; } @@ -137,7 +137,7 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { const configProvider = await this._extHostConfiguration.getConfigProvider(); if (!shellLaunchConfig.executable) { shellLaunchConfig.executable = this.getDefaultShell(false, configProvider); - shellLaunchConfig.args = this._getDefaultShellArgs(false, configProvider); + shellLaunchConfig.args = this.getDefaultShellArgs(false, configProvider); } else { if (this._variableResolver) { shellLaunchConfig.executable = this._variableResolver.resolve(this._lastActiveWorkspace, shellLaunchConfig.executable); @@ -156,7 +156,7 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { } const activeWorkspaceRootUri = URI.revive(activeWorkspaceRootUriComponents); - let lastActiveWorkspace: IWorkspaceFolder | null = null; + let lastActiveWorkspace: IWorkspaceFolder | undefined; if (activeWorkspaceRootUriComponents && activeWorkspaceRootUri) { // Get the environment const apiLastActiveWorkspace = await this._extHostWorkspace.getWorkspaceFolder(activeWorkspaceRootUri); @@ -175,7 +175,7 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { // Get the initial cwd const terminalConfig = configProvider.getConfiguration('terminal.integrated'); - const initialCwd = terminalEnvironment.getCwd(shellLaunchConfig, os.homedir(), lastActiveWorkspace ? lastActiveWorkspace : undefined, this._variableResolver, activeWorkspaceRootUri, terminalConfig.cwd, this._logService); + const initialCwd = terminalEnvironment.getCwd(shellLaunchConfig, os.homedir(), lastActiveWorkspace, this._variableResolver, activeWorkspaceRootUri, terminalConfig.cwd, this._logService); shellLaunchConfig.cwd = initialCwd; const envFromConfig = this._apiInspectConfigToPlain(configProvider.getConfiguration('terminal.integrated').inspect(`env.${platformKey}`)); @@ -208,7 +208,7 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { const configProvider = await this._extHostConfiguration.getConfigProvider(); return Promise.resolve({ shell: this.getDefaultShell(useAutomationShell, configProvider), - args: this._getDefaultShellArgs(useAutomationShell, configProvider) + args: this.getDefaultShellArgs(useAutomationShell, configProvider) }); } diff --git a/src/vs/workbench/api/node/extHostTunnelService.ts b/src/vs/workbench/api/node/extHostTunnelService.ts index 870413fc0a..bf6697c23c 100644 --- a/src/vs/workbench/api/node/extHostTunnelService.ts +++ b/src/vs/workbench/api/node/extHostTunnelService.ts @@ -5,7 +5,7 @@ import { MainThreadTunnelServiceShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { URI } from 'vs/base/common/uri'; @@ -128,7 +128,9 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe const childStat = fs.statSync(childUri.fsPath); if (childStat.isDirectory() && !isNaN(pid)) { const cwd = fs.readlinkSync(resources.joinPath(childUri, 'cwd').fsPath); - const cmd = fs.readFileSync(resources.joinPath(childUri, 'cmdline').fsPath, 'utf8').replace(/\0/g, ' '); + const rawCmd = fs.readFileSync(resources.joinPath(childUri, 'cmdline').fsPath, 'utf8'); + const nullIndex = rawCmd.indexOf('\0'); + const cmd = rawCmd.substr(0, nullIndex > 0 ? nullIndex : rawCmd.length).trim(); processes.push({ pid, cwd, cmd }); } } catch (e) { @@ -181,17 +183,26 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe ip: this.parseIpAddress(address[0]), port: parseInt(address[1], 16) }; - }).map(port => [port.port, port]) + }).map(port => [port.ip + ':' + port.port, port]) ).values() ]; } private parseIpAddress(hex: string): string { let result = ''; - for (let i = hex.length - 2; (i >= 0); i -= 2) { - result += parseInt(hex.substr(i, 2), 16); - if (i !== 0) { - result += '.'; + if (hex.length === 8) { + for (let i = hex.length - 2; i >= 0; i -= 2) { + result += parseInt(hex.substr(i, 2), 16); + if (i !== 0) { + result += '.'; + } + } + } else { + for (let i = hex.length - 4; i >= 0; i -= 4) { + result += parseInt(hex.substr(i, 4), 16).toString(16); + if (i !== 0) { + result += ':'; + } } } return result; diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index 92adcd53d1..52c0c0aa05 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -12,7 +12,6 @@ import { URI } from 'vs/base/common/uri'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { Schemas } from 'vs/base/common/network'; -import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { DefaultEndOfLine } from 'vs/editor/common/model'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEditorViewState } from 'vs/editor/common/editorCommon'; @@ -171,7 +170,6 @@ export class ResourcesDropHandler { @IWorkspacesService private readonly workspacesService: IWorkspacesService, @ITextFileService private readonly textFileService: ITextFileService, @IBackupFileService private readonly backupFileService: IBackupFileService, - @IUntitledTextEditorService private readonly untitledTextEditorService: IUntitledTextEditorService, @IEditorService private readonly editorService: IEditorService, @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService, @@ -244,7 +242,7 @@ export class ResourcesDropHandler { // Untitled: always ensure that we open a new untitled for each file we drop if (droppedDirtyEditor.resource.scheme === Schemas.untitled) { - droppedDirtyEditor.resource = this.untitledTextEditorService.createOrGet(undefined, droppedDirtyEditor.mode, undefined, droppedDirtyEditor.encoding).getResource(); + droppedDirtyEditor.resource = this.textFileService.untitled.create({ mode: droppedDirtyEditor.mode, encoding: droppedDirtyEditor.encoding }).getResource(); } // Return early if the resource is already dirty in target or opened already @@ -352,7 +350,6 @@ export function fillResourceDataTransfers(accessor: ServicesAccessor, resources: // Editors: enables cross window DND of tabs into the editor area const textFileService = accessor.get(ITextFileService); - const untitledTextEditorService = accessor.get(IUntitledTextEditorService); const backupFileService = accessor.get(IBackupFileService); const editorService = accessor.get(IEditorService); @@ -375,16 +372,11 @@ export function fillResourceDataTransfers(accessor: ServicesAccessor, resources: // Try to find encoding and mode from text model let encoding: string | undefined = undefined; let mode: string | undefined = undefined; - if (untitledTextEditorService.exists(file.resource)) { - const model = untitledTextEditorService.createOrGet(file.resource); + + const model = file.resource.scheme === Schemas.untitled ? textFileService.untitled.get(file.resource) : textFileService.files.get(file.resource); + if (model) { encoding = model.getEncoding(); mode = model.getMode(); - } else { - const model = textFileService.models.get(file.resource); - if (model) { - encoding = model.getEncoding(); - mode = model.getMode(); - } } // If the resource is dirty, send over its backup diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index 3f346c9251..dbf57557d2 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -13,7 +13,7 @@ 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 { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; +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'; @@ -260,7 +260,7 @@ class ResourceLabelWidget extends IconLabel { @IModelService private readonly modelService: IModelService, @IDecorationsService private readonly decorationsService: IDecorationsService, @ILabelService private readonly labelService: ILabelService, - @IUntitledTextEditorService private readonly untitledTextEditorService: IUntitledTextEditorService, + @ITextFileService private readonly textFileService: ITextFileService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService ) { super(container, options); @@ -390,7 +390,7 @@ class ResourceLabelWidget extends IconLabel { } let description: string | undefined; - const hidePath = (options && options.hidePath) || (resource.scheme === Schemas.untitled && !this.untitledTextEditorService.hasAssociatedFilePath(resource)); + const hidePath = (options && options.hidePath) || (resource.scheme === Schemas.untitled && !this.textFileService.untitled.hasAssociatedFilePath(resource)); if (!hidePath) { description = this.labelService.getUriLabel(resources.dirname(resource), { relative: true }); } diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 7b47da9c84..5c78c4e079 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -26,7 +26,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { URI, UriComponents } from 'vs/base/common/uri'; import { ToggleCompositePinnedAction, ICompositeBarColors, ActivityAction, ICompositeActivity } from 'vs/workbench/browser/parts/compositeBarActions'; import { ViewletDescriptor } from 'vs/workbench/browser/viewlet'; -import { IViewsService, IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainer, TEST_VIEW_CONTAINER_ID, IViewDescriptorCollection } from 'vs/workbench/common/views'; +import { IViewDescriptorService, IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainer, TEST_VIEW_CONTAINER_ID, IViewDescriptorCollection } from 'vs/workbench/common/views'; import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IViewlet } from 'vs/workbench/common/viewlet'; import { isUndefinedOrNull, assertIsDefined } from 'vs/base/common/types'; @@ -88,7 +88,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { @IThemeService themeService: IThemeService, @IStorageService private readonly storageService: IStorageService, @IExtensionService private readonly extensionService: IExtensionService, - @IViewsService private readonly viewsService: IViewsService, + @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkbenchEnvironmentService workbenchEnvironmentService: IWorkbenchEnvironmentService, @@ -189,8 +189,8 @@ export class ActivitybarPart extends Part implements IActivityBarService { if (viewletDescriptor) { const viewContainer = this.getViewContainer(viewletDescriptor.id); if (viewContainer?.hideIfEmpty) { - const viewDescriptors = this.viewsService.getViewDescriptors(viewContainer); - if (viewDescriptors?.activeViewDescriptors.length === 0) { + const viewDescriptors = this.viewDescriptorService.getViewDescriptors(viewContainer); + if (viewDescriptors.activeViewDescriptors.length === 0) { this.hideComposite(viewletDescriptor.id); // Update the composite bar by hiding } } @@ -410,11 +410,9 @@ export class ActivitybarPart extends Part implements IActivityBarService { this.enableCompositeActions(viewlet); const viewContainer = this.getViewContainer(viewlet.id); if (viewContainer?.hideIfEmpty) { - const viewDescriptors = this.viewsService.getViewDescriptors(viewContainer); - if (viewDescriptors) { - this.onDidChangeActiveViews(viewlet, viewDescriptors); - this.viewletDisposables.set(viewlet.id, viewDescriptors.onDidChangeActiveViews(() => this.onDidChangeActiveViews(viewlet, viewDescriptors))); - } + const viewDescriptors = this.viewDescriptorService.getViewDescriptors(viewContainer); + this.onDidChangeActiveViews(viewlet, viewDescriptors); + this.viewletDisposables.set(viewlet.id, viewDescriptors.onDidChangeActiveViews(() => this.onDidChangeActiveViews(viewlet, viewDescriptors))); } } } @@ -551,11 +549,9 @@ export class ActivitybarPart extends Part implements IActivityBarService { if (viewlet) { const views: { when: string | undefined }[] = []; if (viewContainer) { - const viewDescriptors = this.viewsService.getViewDescriptors(viewContainer); - if (viewDescriptors) { - for (const { when } of viewDescriptors.allViewDescriptors) { - views.push({ when: when ? when.serialize() : undefined }); - } + const viewDescriptors = this.viewDescriptorService.getViewDescriptors(viewContainer); + for (const { when } of viewDescriptors.allViewDescriptors) { + views.push({ when: when ? when.serialize() : undefined }); } } state.push({ id: compositeItem.id, name: viewlet.name, iconUrl: viewlet.iconUrl && viewlet.iconUrl.scheme === Schemas.file ? viewlet.iconUrl : undefined, views, pinned: compositeItem.pinned, order: compositeItem.order, visible: compositeItem.visible }); diff --git a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css index 96ffa37af6..cb3b99ad08 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css +++ b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css @@ -91,6 +91,10 @@ height: 100%; } +.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .active-item-indicator { + pointer-events: none; +} + .monaco-workbench.border .activitybar.right > .content :not(.monaco-menu) > .monaco-action-bar .active-item-indicator { left: -2px; } diff --git a/src/vs/workbench/browser/parts/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositeBarActions.ts index e500122030..14c50c6167 100644 --- a/src/vs/workbench/browser/parts/compositeBarActions.ts +++ b/src/vs/workbench/browser/parts/compositeBarActions.ts @@ -168,7 +168,7 @@ export class ActivityActionViewItem extends BaseActionViewItem { } else { const foreground = this._action.checked ? colors.activeForegroundColor : colors.inactiveForegroundColor; const borderBottomColor = this._action.checked ? colors.activeBorderBottomColor : null; - this.label.style.color = foreground ? foreground.toString() : null; + this.label.style.color = foreground ? foreground.toString() : ''; this.label.style.borderBottomColor = borderBottomColor ? borderBottomColor.toString() : ''; } } @@ -179,7 +179,7 @@ export class ActivityActionViewItem extends BaseActionViewItem { const badgeBackground = colors.badgeBackground; const contrastBorderColor = theme.getColor(contrastBorder); - this.badgeContent.style.color = badgeForeground ? badgeForeground.toString() : null; + this.badgeContent.style.color = badgeForeground ? badgeForeground.toString() : ''; this.badgeContent.style.backgroundColor = badgeBackground ? badgeBackground.toString() : ''; this.badgeContent.style.borderStyle = contrastBorderColor ? 'solid' : ''; diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index f63db5ab33..b53f8b3543 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -411,7 +411,7 @@ export abstract class CompositePart extends Part { }, updateStyles: () => { - titleLabel.style.color = $this.titleForegroundColor ? $this.getColor($this.titleForegroundColor) : null; + titleLabel.style.color = $this.titleForegroundColor ? $this.getColor($this.titleForegroundColor) || '' : ''; } }; } diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts index 429b75151d..b84470df71 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts @@ -71,6 +71,7 @@ export class EditorBreadcrumbsModel { dispose(): void { this._cfgFilePath.dispose(); this._cfgSymbolPath.dispose(); + this._outlineDisposables.dispose(); this._disposables.dispose(); } @@ -192,13 +193,16 @@ export class EditorBreadcrumbsModel { this._outlineDisposables.add({ dispose: () => { - source.cancel(); - source.dispose(); + source.dispose(true); timeout.dispose(); } }); OutlineModel.create(buffer, source.token).then(model => { + if (source.token.isCancellationRequested) { + // cancelled -> do nothing + return; + } if (TreeElement.empty(model)) { // empty -> no outline elements this._updateOutlineElements([]); diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index e2b791673f..0e57b7d1cb 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -632,7 +632,7 @@ export abstract class BaseCloseAllAction extends Action { // can review if the files should be changed or not. await Promise.all(this.groupsToClose.map(async groupToClose => { for (const editor of groupToClose.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)) { - if (editor.isDirty()) { + if (editor.isDirty() && !editor.isSaving() /* ignore editors that are being saved */) { return groupToClose.openEditor(editor); } } @@ -644,8 +644,8 @@ export abstract class BaseCloseAllAction extends Action { const dirtyEditorsToConfirmByResource = new ResourceMap(); for (const editor of this.editorService.editors) { - if (!editor.isDirty()) { - continue; // only interested in dirty editors + if (!editor.isDirty() || editor.isSaving()) { + continue; // only interested in dirty editors (unless in the process of saving) } const resource = editor.getResource(); diff --git a/src/vs/workbench/browser/parts/editor/editorAutoSave.ts b/src/vs/workbench/browser/parts/editor/editorAutoSave.ts index 65ba4b9979..1a159a1d9c 100644 --- a/src/vs/workbench/browser/parts/editor/editorAutoSave.ts +++ b/src/vs/workbench/browser/parts/editor/editorAutoSave.ts @@ -4,16 +4,23 @@ *--------------------------------------------------------------------------------------------*/ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { Disposable, DisposableStore, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; +import { IFilesConfigurationService, AutoSaveMode, IAutoSaveConfiguration } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { SaveReason, IEditorIdentifier, IEditorInput, GroupIdentifier } from 'vs/workbench/common/editor'; +import { SaveReason, IEditorIdentifier, IEditorInput, GroupIdentifier, ISaveOptions } from 'vs/workbench/common/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { withNullAsUndefined } from 'vs/base/common/types'; +import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { ILogService } from 'vs/platform/log/common/log'; export class EditorAutoSave extends Disposable implements IWorkbenchContribution { + // Auto save: after delay + private autoSaveAfterDelay: number | undefined; + private readonly pendingAutoSavesAfterDelay = new Map(); + + // Auto save: focus change & window change private lastActiveEditor: IEditorInput | undefined = undefined; private lastActiveGroupId: GroupIdentifier | undefined = undefined; private lastActiveEditorControlDisposable = this._register(new DisposableStore()); @@ -22,16 +29,28 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, @IHostService private readonly hostService: IHostService, @IEditorService private readonly editorService: IEditorService, - @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService + @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, + @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, + @ILogService private readonly logService: ILogService ) { super(); + // Figure out initial auto save config + this.onAutoSaveConfigurationChange(filesConfigurationService.getAutoSaveConfiguration(), false); + this.registerListeners(); } private registerListeners(): void { this._register(this.hostService.onDidChangeFocus(focused => this.onWindowFocusChange(focused))); this._register(this.editorService.onDidActiveEditorChange(() => this.onDidActiveEditorChange())); + this._register(this.filesConfigurationService.onAutoSaveConfigurationChange(config => this.onAutoSaveConfigurationChange(config, true))); + + // Working Copy events + this._register(this.workingCopyService.onDidRegister(c => this.onDidRegister(c))); + this._register(this.workingCopyService.onDidUnregister(c => this.onDidUnregister(c))); + this._register(this.workingCopyService.onDidChangeDirty(c => this.onDidChangeDirty(c))); + this._register(this.workingCopyService.onDidChangeContent(c => this.onDidChangeContent(c))); } private onWindowFocusChange(focused: boolean): void { @@ -76,11 +95,109 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution (reason === SaveReason.WINDOW_CHANGE && (mode === AutoSaveMode.ON_FOCUS_CHANGE || mode === AutoSaveMode.ON_WINDOW_CHANGE)) || (reason === SaveReason.FOCUS_CHANGE && mode === AutoSaveMode.ON_FOCUS_CHANGE) ) { + this.logService.trace(`[editor auto save] triggering auto save with reason ${reason}`); + if (editorIdentifier) { this.editorService.save(editorIdentifier, { reason }); } else { - this.editorService.saveAll({ reason }); + this.saveAllDirty({ reason }); } } } + + private onAutoSaveConfigurationChange(config: IAutoSaveConfiguration, fromEvent: boolean): void { + + // Update auto save after delay config + this.autoSaveAfterDelay = (typeof config.autoSaveDelay === 'number') && config.autoSaveDelay > 0 ? config.autoSaveDelay : undefined; + + // Trigger a save-all when auto save is enabled + if (fromEvent) { + let reason: SaveReason | undefined = undefined; + switch (this.filesConfigurationService.getAutoSaveMode()) { + case AutoSaveMode.ON_FOCUS_CHANGE: + reason = SaveReason.FOCUS_CHANGE; + break; + case AutoSaveMode.ON_WINDOW_CHANGE: + reason = SaveReason.WINDOW_CHANGE; + break; + case AutoSaveMode.AFTER_SHORT_DELAY: + case AutoSaveMode.AFTER_LONG_DELAY: + reason = SaveReason.AUTO; + break; + } + + if (reason) { + this.saveAllDirty({ reason }); + } + } + } + + private saveAllDirty(options?: ISaveOptions): void { + for (const workingCopy of this.workingCopyService.dirtyWorkingCopies) { + if (!(workingCopy.capabilities & WorkingCopyCapabilities.Untitled)) { + workingCopy.save(options); + } + } + } + + private onDidRegister(workingCopy: IWorkingCopy): void { + this.scheduleAutoSave(workingCopy); + } + + private onDidUnregister(workingCopy: IWorkingCopy): void { + this.discardAutoSave(workingCopy); + } + + private onDidChangeDirty(workingCopy: IWorkingCopy): void { + if (!workingCopy.isDirty()) { + this.discardAutoSave(workingCopy); + } + } + + private onDidChangeContent(workingCopy: IWorkingCopy): void { + if (workingCopy.isDirty()) { + this.scheduleAutoSave(workingCopy); + } + } + + private scheduleAutoSave(workingCopy: IWorkingCopy): void { + if (typeof this.autoSaveAfterDelay !== 'number') { + return; // auto save after delay must be enabled + } + + if (workingCopy.capabilities & WorkingCopyCapabilities.Untitled) { + return; // we never auto save untitled working copies + } + + // Clear any running auto save operation + this.discardAutoSave(workingCopy); + + this.logService.trace(`[editor auto save] scheduling auto save after ${this.autoSaveAfterDelay}ms`, workingCopy.resource.toString()); + + // Schedule new auto save + const handle = setTimeout(() => { + + // Clear disposable + this.pendingAutoSavesAfterDelay.delete(workingCopy); + + // Save if dirty + if (workingCopy.isDirty()) { + this.logService.trace(`[editor auto save] running auto save`, workingCopy.resource.toString()); + + workingCopy.save({ reason: SaveReason.AUTO }); + } + }, this.autoSaveAfterDelay); + + // Keep in map for disposal as needed + this.pendingAutoSavesAfterDelay.set(workingCopy, toDisposable(() => { + this.logService.trace(`[editor auto save] clearing pending auto save`, workingCopy.resource.toString()); + + clearTimeout(handle); + })); + } + + private discardAutoSave(workingCopy: IWorkingCopy): void { + dispose(this.pendingAutoSavesAfterDelay.get(workingCopy)); + this.pendingAutoSavesAfterDelay.delete(workingCopy); + } } diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index 0f11d331f4..c12ab0f305 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -18,7 +18,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { RunOnceScheduler } from 'vs/base/common/async'; import { find } from 'vs/base/common/arrays'; import { DataTransfers } from 'vs/base/browser/dnd'; -import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { VSBuffer } from 'vs/base/common/buffer'; import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { URI } from 'vs/base/common/uri'; @@ -48,7 +48,7 @@ class DropOverlay extends Themable { private groupView: IEditorGroupView, @IThemeService themeService: IThemeService, @IInstantiationService private instantiationService: IInstantiationService, - @IUntitledTextEditorService private untitledTextEditorService: IUntitledTextEditorService, + @ITextFileService private textFileService: ITextFileService, @IFileDialogService private readonly fileDialogService: IFileDialogService ) { super(themeService); @@ -309,7 +309,7 @@ class DropOverlay extends Themable { // Open as untitled file with the provided contents const contents = VSBuffer.wrap(new Uint8Array(event.target.result)).toString(); - const untitledEditor = this.untitledTextEditorService.createOrGet(proposedFilePath, undefined, contents); + const untitledEditor = this.textFileService.untitled.create({ associatedResource: proposedFilePath, initialValue: contents }); if (!targetGroup) { targetGroup = ensureTargetGroup(); diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 9cec4c8211..db43d43610 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -32,7 +32,7 @@ import { RunOnceWorker } from 'vs/base/common/async'; import { EventType as TouchEventType, GestureEvent } from 'vs/base/browser/touch'; import { TitleControl } from 'vs/workbench/browser/parts/editor/titleControl'; import { IEditorGroupsAccessor, IEditorGroupView, getActiveTextEditorOptions, IEditorOpeningEvent } from 'vs/workbench/browser/parts/editor/editor'; -import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ActionRunner, IAction, Action } from 'vs/base/common/actions'; @@ -129,7 +129,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { @INotificationService private readonly notificationService: INotificationService, @IDialogService private readonly dialogService: IDialogService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IUntitledTextEditorService private readonly untitledTextEditorService: IUntitledTextEditorService, + @ITextFileService private readonly textFileService: ITextFileService, @IKeybindingService private readonly keybindingService: IKeybindingService, @IMenuService private readonly menuService: IMenuService, @IContextMenuService private readonly contextMenuService: IContextMenuService, @@ -226,8 +226,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { const activeEditor = this._group.activeEditor; if (activeEditor) { - groupActiveEditorDirtyContextKey.set(activeEditor.isDirty()); - activeEditorListener.value = activeEditor.onDidChangeDirty(() => groupActiveEditorDirtyContextKey.set(activeEditor.isDirty())); + groupActiveEditorDirtyContextKey.set(activeEditor.isDirty() && !activeEditor.isSaving()); + activeEditorListener.value = activeEditor.onDidChangeDirty(() => groupActiveEditorDirtyContextKey.set(activeEditor.isDirty() && !activeEditor.isSaving())); } else { groupActiveEditorDirtyContextKey.set(false); } @@ -256,7 +256,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { if (this.isEmpty) { EventHelper.stop(e); - this.openEditor(this.untitledTextEditorService.createOrGet(), EditorOptions.create({ pinned: true })); + this.openEditor(this.textFileService.untitled.create(), EditorOptions.create({ pinned: true })); } })); @@ -823,6 +823,13 @@ export class EditorGroupView extends Themable implements IEditorGroupView { private doOpenEditor(editor: EditorInput, options?: EditorOptions): Promise { + // Guard against invalid inputs. Disposed inputs + // should never open because they emit no events + // e.g. to indicate dirty changes. + if (editor.isDisposed()) { + return Promise.resolve(undefined); + } + // Determine options const openEditorOptions: IEditorOpenOptions = { index: options ? options.index : undefined, @@ -859,15 +866,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { restoreGroup = !activateGroup; } - // Do this before we open the editor in the group to prevent a false - // active editor change event before the editor is loaded - // (see https://github.com/Microsoft/vscode/issues/51679) - if (activateGroup) { - this.accessor.activateGroup(this); - } else if (restoreGroup) { - this.accessor.restoreGroup(this); - } - // Actually move the editor if a specific index is provided and we figure // out that the editor is already opened at a different index. This // ensures the right set of events are fired to the outside. @@ -884,7 +882,16 @@ export class EditorGroupView extends Themable implements IEditorGroupView { const openedEditor = this._group.openEditor(editor, openEditorOptions); // Show editor - return this.doShowEditor(openedEditor, !!openEditorOptions.active, options); + const showEditorResult = this.doShowEditor(openedEditor, !!openEditorOptions.active, options); + + // Finally make sure the group is active or restored as instructed + if (activateGroup) { + this.accessor.activateGroup(this); + } else if (restoreGroup) { + this.accessor.restoreGroup(this); + } + + return showEditorResult; } private async doShowEditor(editor: EditorInput, active: boolean, options?: EditorOptions): Promise { @@ -1266,8 +1273,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } private async doHandleDirtyClosing(editor: EditorInput): Promise { - if (!editor.isDirty()) { - return false; // editor must be dirty + if (!editor.isDirty() || editor.isSaving()) { + return false; // editor must be dirty and not saving } if (editor instanceof SideBySideEditorInput && this.isOpened(editor.master)) { @@ -1304,11 +1311,11 @@ export class EditorGroupView extends Themable implements IEditorGroupView { const editorResource = toResource(editor, { supportSideBySide: SideBySideEditor.MASTER }); const res = await this.fileDialogService.showSaveConfirm(editorResource ? [editorResource] : [editor.getName()]); - // It could be that the editor saved meanwhile, so we check again - // to see if anything needs to happen before closing for good. + // It could be that the editor saved meanwhile or is saving, so we check + // again to see if anything needs to happen before closing for good. // This can happen for example if autoSave: onFocusChange is configured // so that the save happens when the dialog opens. - if (!editor.isDirty()) { + if (!editor.isDirty() || editor.isSaving()) { return res === ConfirmResult.CANCEL ? true : false; } @@ -1371,9 +1378,9 @@ export class EditorGroupView extends Themable implements IEditorGroupView { let editorsToClose = this._group.getEditors(hasDirection ? EditorsOrder.SEQUENTIAL : EditorsOrder.MOST_RECENTLY_ACTIVE); // in MRU order only if direction is not specified - // Filter: saved only + // Filter: saved or saving only if (filter.savedOnly) { - editorsToClose = editorsToClose.filter(e => !e.isDirty()); + editorsToClose = editorsToClose.filter(e => !e.isDirty() || e.isSaving()); } // Filter: direction (left / right) @@ -1466,7 +1473,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { let activeReplacement: EditorReplacement | undefined; const inactiveReplacements: EditorReplacement[] = []; editors.forEach(({ editor, replacement, options }) => { - if (editor.isDirty()) { + if (editor.isDirty() && !editor.isSaving()) { return; // we do not handle dirty in this method, so ignore all dirty } @@ -1494,10 +1501,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { }); // Handle inactive first - inactiveReplacements.forEach(({ editor, replacement, options }) => { + inactiveReplacements.forEach(async ({ editor, replacement, options }) => { // Open inactive editor - this.openEditor(replacement, options); // {{SQL CARBON EDIT}} use this.openEditor to allow us to override the open, we could potentially add this to vscode but i don't think they would care + await this.openEditor(replacement, options); // {{SQL CARBON EDIT}} use this.openEditor to allow us to override the open, we could potentially add this to vscode but i don't think they would care // Close replaced inactive editor unless they match if (!editor.matches(replacement)) { diff --git a/src/vs/workbench/browser/parts/editor/editorPicker.ts b/src/vs/workbench/browser/parts/editor/editorPicker.ts index a66789fcf6..bf607c3c1a 100644 --- a/src/vs/workbench/browser/parts/editor/editorPicker.ts +++ b/src/vs/workbench/browser/parts/editor/editorPicker.ts @@ -42,7 +42,7 @@ export class EditorPickerEntry extends QuickOpenEntryGroup { } getIcon(): string { - return this.editor.isDirty() ? 'dirty' : ''; + return this.editor.isDirty() && !this.editor.isSaving() ? 'codicon codicon-circle-filled' : ''; } get group(): IEditorGroup { diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index 3edb3b308b..fd97a650a3 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -15,7 +15,6 @@ import { Language } from 'vs/base/common/platform'; import { UntitledTextEditorInput } from 'vs/workbench/common/editor/untitledTextEditorInput'; import { IFileEditorInput, EncodingMode, IEncodingSupport, toResource, SideBySideEditorInput, IEditor as IBaseEditor, IEditorInput, SideBySideEditor, IModeSupport } from 'vs/workbench/common/editor'; import { Disposable, MutableDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { IEditorAction } from 'vs/editor/common/editorCommon'; import { EndOfLineSequence } from 'vs/editor/common/model'; import { IModelLanguageChangedEvent, IModelOptionsChangedEvent } from 'vs/editor/common/model/textModelEvents'; @@ -299,7 +298,6 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { constructor( @IEditorService private readonly editorService: IEditorService, @IQuickInputService private readonly quickInputService: IQuickInputService, - @IUntitledTextEditorService private readonly untitledTextEditorService: IUntitledTextEditorService, @IModeService private readonly modeService: IModeService, @ITextFileService private readonly textFileService: ITextFileService, @IConfigurationService private readonly configurationService: IConfigurationService, @@ -316,8 +314,8 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { private registerListeners(): void { this._register(this.editorService.onDidActiveEditorChange(() => this.updateStatusBar())); - this._register(this.untitledTextEditorService.onDidChangeEncoding(r => this.onResourceEncodingChange(r))); - this._register(this.textFileService.models.onModelEncodingChanged(e => this.onResourceEncodingChange((e.resource)))); + this._register(this.textFileService.untitled.onDidChangeEncoding(r => this.onResourceEncodingChange(r))); + this._register(this.textFileService.files.onDidChangeEncoding(m => this.onResourceEncodingChange((m.resource)))); this._register(TabFocus.onDidChangeTabFocus(e => this.onTabFocusModeChange())); } @@ -1004,7 +1002,7 @@ export class ChangeModeAction extends Action { @IQuickInputService private readonly quickInputService: IQuickInputService, @IPreferencesService private readonly preferencesService: IPreferencesService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IUntitledTextEditorService private readonly untitledTextEditorService: IUntitledTextEditorService + @ITextFileService private readonly textFileService: ITextFileService ) { super(actionId, actionLabel); } @@ -1019,7 +1017,7 @@ export class ChangeModeAction extends Action { const resource = this.editorService.activeEditor ? toResource(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.MASTER }) : null; let hasLanguageSupport = !!resource; - if (resource?.scheme === Schemas.untitled && !this.untitledTextEditorService.hasAssociatedFilePath(resource)) { + if (resource?.scheme === Schemas.untitled && !this.textFileService.untitled.hasAssociatedFilePath(resource)) { hasLanguageSupport = false; // no configuration for untitled resources (e.g. "Untitled-1") } diff --git a/src/vs/workbench/browser/parts/editor/editorsObserver.ts b/src/vs/workbench/browser/parts/editor/editorsObserver.ts index 6db8be0701..5a5bdbeb29 100644 --- a/src/vs/workbench/browser/parts/editor/editorsObserver.ts +++ b/src/vs/workbench/browser/parts/editor/editorsObserver.ts @@ -260,8 +260,8 @@ export class EditorsObserver extends Disposable { // Extract least recently used editors that can be closed const leastRecentlyClosableEditors = mostRecentEditors.reverse().filter(({ editor, groupId }) => { - if (editor.isDirty()) { - return false; // not dirty editors + if (editor.isDirty() && !editor.isSaving()) { + return false; // not dirty editors (unless in the process of saving) } if (exclude && editor === exclude.editor && groupId === exclude.groupId) { diff --git a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css b/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css index 875c19f5dc..334cb1f596 100644 --- a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css @@ -159,15 +159,15 @@ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fit .monaco-icon-label, -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fit .monaco-icon-label > .monaco-icon-label-description-container { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fit .monaco-icon-label > .monaco-icon-label-container > .monaco-icon-name-container { overflow: visible; /* fixes https://github.com/Microsoft/vscode/issues/20182 */ } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink > .monaco-icon-label > .monaco-icon-label-description-container { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink > .monaco-icon-label > .monaco-icon-label-container > .monaco-icon-name-container { text-overflow: clip; } -.hc-black .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink > .monaco-icon-label > .monaco-icon-label-description-container { +.hc-black .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink > .monaco-icon-label > .monaco-icon-label-container > .monaco-icon-name-container { text-overflow: ellipsis; } diff --git a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts index 7711ae4df2..1df102895d 100644 --- a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts @@ -166,9 +166,14 @@ export class NoTabsTitleControl extends TitleControl { updateEditorDirty(editor: IEditorInput): void { this.ifEditorIsActive(editor, () => { const titleContainer = assertIsDefined(this.titleContainer); - if (editor.isDirty()) { + + // Signal dirty (unless saving) + if (editor.isDirty() && !editor.isSaving()) { addClass(titleContainer, 'dirty'); - } else { + } + + // Otherwise, clear dirty + else { removeClass(titleContainer, 'dirty'); } }); @@ -275,9 +280,9 @@ export class NoTabsTitleControl extends TitleControl { editorLabel.setResource({ name, description, resource }, { title: typeof title === 'string' ? title : undefined, italic: !isEditorPinned, extraClasses: ['no-tabs', 'title-label'] }); if (isGroupActive) { - editorLabel.element.style.color = this.getColor(TAB_ACTIVE_FOREGROUND); + editorLabel.element.style.color = this.getColor(TAB_ACTIVE_FOREGROUND) || ''; } else { - editorLabel.element.style.color = this.getColor(TAB_UNFOCUSED_ACTIVE_FOREGROUND); + editorLabel.element.style.color = this.getColor(TAB_UNFOCUSED_ACTIVE_FOREGROUND) || ''; } // Update Editor Actions Toolbar diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index ed953d3175..6e82aa422b 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -32,7 +32,6 @@ import { Color } from 'vs/base/common/color'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { MergeGroupMode, IMergeGroupOptions, GroupsArrangement } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { addClass, addDisposableListener, hasClass, EventType, EventHelper, removeClass, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode } from 'vs/base/browser/dom'; import { localize } from 'vs/nls'; import { IEditorGroupsAccessor, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; @@ -42,6 +41,7 @@ import { BreadcrumbsControl } from 'vs/workbench/browser/parts/editor/breadcrumb import { IFileService } from 'vs/platform/files/common/files'; import { withNullAsUndefined, assertAllDefined, assertIsDefined } from 'vs/base/common/types'; import { ILabelService } from 'vs/platform/label/common/label'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; // {{SQL CARBON EDIT}} -- Display the editor's tab color import * as QueryConstants from 'sql/workbench/contrib/query/common/constants'; @@ -79,7 +79,7 @@ export class TabsTitleControl extends TitleControl { group: IEditorGroupView, @IContextMenuService contextMenuService: IContextMenuService, @IInstantiationService instantiationService: IInstantiationService, - @IUntitledTextEditorService private readonly untitledTextEditorService: IUntitledTextEditorService, + @ITextFileService private readonly textFileService: ITextFileService, @IContextKeyService contextKeyService: IContextKeyService, @IKeybindingService keybindingService: IKeybindingService, @ITelemetryService telemetryService: ITelemetryService, @@ -197,7 +197,7 @@ export class TabsTitleControl extends TitleControl { EventHelper.stop(e); - this.group.openEditor(this.untitledTextEditorService.createOrGet(), { pinned: true /* untitled is always pinned */, index: this.group.count /* always at the end */ }); + this.group.openEditor(this.textFileService.untitled.create(), { pinned: true /* untitled is always pinned */, index: this.group.count /* always at the end */ }); })); }); @@ -1001,7 +1001,7 @@ export class TabsTitleControl extends TitleControl { } // Label - tabLabelWidget.element.style.color = this.getColor(isGroupActive ? TAB_ACTIVE_FOREGROUND : TAB_UNFOCUSED_ACTIVE_FOREGROUND); + tabLabelWidget.element.style.color = this.getColor(isGroupActive ? TAB_ACTIVE_FOREGROUND : TAB_UNFOCUSED_ACTIVE_FOREGROUND) || ''; } // Tab is inactive @@ -1014,7 +1014,7 @@ export class TabsTitleControl extends TitleControl { tabContainer.style.boxShadow = ''; // Label - tabLabelWidget.element.style.color = this.getColor(isGroupActive ? TAB_INACTIVE_FOREGROUND : TAB_UNFOCUSED_INACTIVE_FOREGROUND); + tabLabelWidget.element.style.color = this.getColor(isGroupActive ? TAB_INACTIVE_FOREGROUND : TAB_UNFOCUSED_INACTIVE_FOREGROUND) || ''; } // {{SQL CARBON EDIT}} - Override the editor's tab color if query tab coloring is set @@ -1024,8 +1024,8 @@ export class TabsTitleControl extends TitleControl { private doRedrawEditorDirty(isGroupActive: boolean, isTabActive: boolean, editor: IEditorInput, tabContainer: HTMLElement): boolean { let hasModifiedBorderColor = false; - // Tab: dirty - if (editor.isDirty()) { + // Tab: dirty (unless saving) + if (editor.isDirty() && !editor.isSaving()) { addClass(tabContainer, 'dirty'); // Highlight modified tabs with a border if configured diff --git a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index 36545ab275..c13dd0dff5 100644 --- a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -26,7 +26,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { EditorMemento } from 'vs/workbench/browser/parts/editor/baseEditor'; import { EditorActivation, IEditorOptions } from 'vs/platform/editor/common/editor'; @@ -192,7 +192,8 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor { options = EditorOptions.create(preservingOptions); } - this.editorService.openEditor(binaryDiffInput, options, this.group); + // Replace this editor with the binary one + this.editorService.replaceEditors([{ editor: input, replacement: binaryDiffInput, options }], this.group || ACTIVE_GROUP); return true; } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts index 14d4c84f2b..d5851af773 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts @@ -236,7 +236,7 @@ export class NotificationsCenter extends Themable { this.notificationsCenterContainer.style.border = borderColor ? `1px solid ${borderColor}` : ''; const headerForeground = this.getColor(NOTIFICATIONS_CENTER_HEADER_FOREGROUND); - this.notificationsCenterHeader.style.color = headerForeground ? headerForeground.toString() : null; + this.notificationsCenterHeader.style.color = headerForeground ? headerForeground.toString() : ''; const headerBackground = this.getColor(NOTIFICATIONS_CENTER_HEADER_BACKGROUND); this.notificationsCenterHeader.style.background = headerBackground ? headerBackground.toString() : ''; diff --git a/src/vs/workbench/browser/parts/notifications/notificationsList.ts b/src/vs/workbench/browser/parts/notifications/notificationsList.ts index cec8ad6cb6..47bdea2b89 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsList.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsList.ts @@ -221,7 +221,7 @@ export class NotificationsList extends Themable { protected updateStyles(): void { if (this.listContainer) { const foreground = this.getColor(NOTIFICATIONS_FOREGROUND); - this.listContainer.style.color = foreground ? foreground : null; + this.listContainer.style.color = foreground ? foreground : ''; const background = this.getColor(NOTIFICATIONS_BACKGROUND); this.listContainer.style.background = background ? background : ''; diff --git a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts index 9e05721438..65433efae0 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts @@ -525,7 +525,7 @@ export class NotificationsToasts extends Themable { // Hide or show toast based on context this.setVisibility(toast, makeVisible); - toast.container.style.opacity = null; + toast.container.style.opacity = ''; if (makeVisible) { visibleToasts++; diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index 4c3ec0f4e8..5db66ea1a6 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -33,7 +33,7 @@ import { IContextKey, IContextKeyService, ContextKeyExpr } from 'vs/platform/con import { isUndefinedOrNull, assertIsDefined } from 'vs/base/common/types'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { ViewContainer, IViewContainersRegistry, Extensions as ViewContainerExtensions, IViewsService, IViewDescriptorCollection } from 'vs/workbench/common/views'; +import { ViewContainer, IViewContainersRegistry, Extensions as ViewContainerExtensions, IViewDescriptorService, IViewDescriptorCollection } from 'vs/workbench/common/views'; interface ICachedPanel { id: string; @@ -98,9 +98,9 @@ export class PanelPart extends CompositePart implements IPanelService { @IKeybindingService keybindingService: IKeybindingService, @IInstantiationService instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, + @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IExtensionService private readonly extensionService: IExtensionService, - @IViewsService private readonly viewsService: IViewsService, ) { super( notificationService, @@ -184,11 +184,9 @@ export class PanelPart extends CompositePart implements IPanelService { this.enableCompositeActions(panel); const viewContainer = this.getViewContainer(panel.id); if (viewContainer?.hideIfEmpty) { - const viewDescriptors = this.viewsService.getViewDescriptors(viewContainer); - if (viewDescriptors) { - this.onDidChangeActiveViews(panel, viewDescriptors); - this.panelDisposables.set(panel.id, viewDescriptors.onDidChangeActiveViews(() => this.onDidChangeActiveViews(panel, viewDescriptors))); - } + const viewDescriptors = this.viewDescriptorService.getViewDescriptors(viewContainer); + this.onDidChangeActiveViews(panel, viewDescriptors); + this.panelDisposables.set(panel.id, viewDescriptors.onDidChangeActiveViews(() => this.onDidChangeActiveViews(panel, viewDescriptors))); } } } @@ -295,8 +293,8 @@ export class PanelPart extends CompositePart implements IPanelService { if (panelDescriptor) { const viewContainer = this.getViewContainer(panelDescriptor.id); if (viewContainer?.hideIfEmpty) { - const viewDescriptors = this.viewsService.getViewDescriptors(viewContainer); - if (viewDescriptors?.activeViewDescriptors.length === 0) { + const viewDescriptors = this.viewDescriptorService.getViewDescriptors(viewContainer); + if (viewDescriptors.activeViewDescriptors.length === 0) { this.hideComposite(panelDescriptor.id); // Update the composite bar by hiding } } diff --git a/src/vs/workbench/browser/parts/quickinput/media/quickInput.css b/src/vs/workbench/browser/parts/quickinput/media/quickInput.css index bc07da5c68..d2fe022be2 100644 --- a/src/vs/workbench/browser/parts/quickinput/media/quickInput.css +++ b/src/vs/workbench/browser/parts/quickinput/media/quickInput.css @@ -45,6 +45,10 @@ background-repeat: no-repeat; } +.quick-input-description { + margin: 6px; +} + .quick-input-header { display: flex; padding: 6px 6px 0px 6px; diff --git a/src/vs/workbench/browser/parts/quickinput/quickInput.ts b/src/vs/workbench/browser/parts/quickinput/quickInput.ts index 2e11ed1e57..9170cafacf 100644 --- a/src/vs/workbench/browser/parts/quickinput/quickInput.ts +++ b/src/vs/workbench/browser/parts/quickinput/quickInput.ts @@ -64,6 +64,7 @@ interface QuickInputUI { leftActionBar: ActionBar; titleBar: HTMLElement; title: HTMLElement; + description: HTMLElement; rightActionBar: ActionBar; checkAll: HTMLInputElement; filterContainer: HTMLElement; @@ -95,6 +96,7 @@ interface QuickInputUI { type Visibilities = { title?: boolean; + description?: boolean; checkAll?: boolean; inputBox?: boolean; visibleCount?: boolean; @@ -108,6 +110,7 @@ type Visibilities = { class QuickInput extends Disposable implements IQuickInput { private _title: string | undefined; + private _description: string | undefined; private _steps: number | undefined; private _totalSteps: number | undefined; protected visible = false; @@ -139,6 +142,15 @@ class QuickInput extends Disposable implements IQuickInput { this.update(); } + get description() { + return this._description; + } + + set description(description: string | undefined) { + this._description = description; + this.update(); + } + get step() { return this._steps; } @@ -244,6 +256,10 @@ class QuickInput extends Disposable implements IQuickInput { if (this.ui.title.textContent !== title) { this.ui.title.textContent = title; } + const description = this.getDescription(); + if (this.ui.description.textContent !== description) { + this.ui.description.textContent = description; + } if (this.busy && !this.busyDelay) { this.busyDelay = new TimeoutTimer(); this.busyDelay.setIfNotSet(() => { @@ -298,6 +314,10 @@ class QuickInput extends Disposable implements IQuickInput { return ''; } + private getDescription() { + return this.description || ''; + } + private getSteps() { if (this.step && this.totalSteps) { return localize('quickInput.steps', "{0}/{1}", this.step, this.totalSteps); @@ -713,7 +733,7 @@ class QuickPick extends QuickInput implements IQuickPi if (!this.visible) { return; } - this.ui.setVisibilities(this.canSelectMany ? { title: !!this.title || !!this.step, checkAll: true, inputBox: true, visibleCount: true, count: true, ok: true, list: true, message: !!this.validationMessage, customButton: this.customButton } : { title: !!this.title || !!this.step, inputBox: true, visibleCount: true, list: true, message: !!this.validationMessage, customButton: this.customButton, ok: this.ok }); + this.ui.setVisibilities(this.canSelectMany ? { title: !!this.title || !!this.step, description: !!this.description, checkAll: true, inputBox: true, visibleCount: true, count: true, ok: this.ok, list: true, message: !!this.validationMessage, customButton: this.customButton } : { title: !!this.title || !!this.step, description: !!this.description, inputBox: true, visibleCount: true, list: true, message: !!this.validationMessage, customButton: this.customButton, ok: this.ok }); super.update(); if (this.ui.inputBox.value !== this.value) { this.ui.inputBox.value = this.value; @@ -872,7 +892,7 @@ class InputBox extends QuickInput implements IInputBox { if (!this.visible) { return; } - this.ui.setVisibilities({ title: !!this.title || !!this.step, inputBox: true, message: true }); + this.ui.setVisibilities({ title: !!this.title || !!this.step, description: !!this.description || !!this.step, inputBox: true, message: true }); super.update(); if (this.ui.inputBox.value !== this.value) { this.ui.inputBox.value = this.value; @@ -1037,6 +1057,8 @@ export class QuickInputService extends Component implements IQuickInputService { const rightActionBar = this._register(new ActionBar(titleBar)); rightActionBar.domNode.classList.add('quick-input-right-action-bar'); + const description = dom.append(container, $('.quick-input-description')); + const headerContainer = dom.append(container, $('.quick-input-header')); const checkAll = dom.append(headerContainer, $('input.quick-input-check-all')); @@ -1166,6 +1188,7 @@ export class QuickInputService extends Component implements IQuickInputService { leftActionBar, titleBar, title, + description, rightActionBar, checkAll, filterContainer, @@ -1377,6 +1400,7 @@ export class QuickInputService extends Component implements IQuickInputService { this.setEnabled(true); ui.leftActionBar.clear(); ui.title.textContent = ''; + ui.description.textContent = ''; ui.rightActionBar.clear(); ui.checkAll.checked = false; // ui.inputBox.value = ''; Avoid triggering an event. @@ -1410,6 +1434,7 @@ export class QuickInputService extends Component implements IQuickInputService { private setVisibilities(visibilities: Visibilities) { const ui = this.getUI(); ui.title.style.display = visibilities.title ? '' : 'none'; + ui.description.style.display = visibilities.description ? '' : 'none'; ui.checkAll.style.display = visibilities.checkAll ? '' : 'none'; ui.filterContainer.style.display = visibilities.inputBox ? '' : 'none'; ui.visibleCountContainer.style.display = visibilities.visibleCount ? '' : 'none'; @@ -1542,7 +1567,7 @@ export class QuickInputService extends Component implements IQuickInputService { const quickInputBackground = theme.getColor(QUICK_INPUT_BACKGROUND); this.ui.container.style.backgroundColor = quickInputBackground ? quickInputBackground.toString() : ''; const quickInputForeground = theme.getColor(QUICK_INPUT_FOREGROUND); - this.ui.container.style.color = quickInputForeground ? quickInputForeground.toString() : null; + this.ui.container.style.color = quickInputForeground ? quickInputForeground.toString() : ''; const contrastBorderColor = theme.getColor(contrastBorder); this.ui.container.style.border = contrastBorderColor ? `1px solid ${contrastBorderColor}` : ''; const widgetShadowColor = theme.getColor(widgetShadow); diff --git a/src/vs/workbench/browser/parts/quickinput/quickInputBox.ts b/src/vs/workbench/browser/parts/quickinput/quickInputBox.ts index c25d57b521..55ca955a79 100644 --- a/src/vs/workbench/browser/parts/quickinput/quickInputBox.ts +++ b/src/vs/workbench/browser/parts/quickinput/quickInputBox.ts @@ -32,17 +32,17 @@ export class QuickInputBox extends Disposable { return dom.addDisposableListener(this.inputBox.inputElement, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { handler(new StandardKeyboardEvent(e)); }); - } + }; onMouseDown = (handler: (event: StandardMouseEvent) => void): IDisposable => { return dom.addDisposableListener(this.inputBox.inputElement, dom.EventType.MOUSE_DOWN, (e: MouseEvent) => { handler(new StandardMouseEvent(e)); }); - } + }; onDidChange = (handler: (event: string) => void): IDisposable => { return this.inputBox.onDidChange(handler); - } + }; get value() { return this.inputBox.value; diff --git a/src/vs/workbench/browser/parts/quickopen/media/dirty-dark.svg b/src/vs/workbench/browser/parts/quickopen/media/dirty-dark.svg deleted file mode 100644 index 51946be5bb..0000000000 --- a/src/vs/workbench/browser/parts/quickopen/media/dirty-dark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/browser/parts/quickopen/media/dirty-light.svg b/src/vs/workbench/browser/parts/quickopen/media/dirty-light.svg deleted file mode 100644 index fb91225b96..0000000000 --- a/src/vs/workbench/browser/parts/quickopen/media/dirty-light.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/vs/workbench/browser/parts/quickopen/media/quickopen.css b/src/vs/workbench/browser/parts/quickopen/media/quickopen.css index 4ade000444..d3b3e5b34c 100644 --- a/src/vs/workbench/browser/parts/quickopen/media/quickopen.css +++ b/src/vs/workbench/browser/parts/quickopen/media/quickopen.css @@ -13,12 +13,3 @@ width: 14px; height: 18px; } - -.vs .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.dirty { - background-image: url('dirty-light.svg'); -} - -.hc-black .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.dirty, -.vs-dark .monaco-workbench .monaco-quick-open-widget .quick-open-tree .quick-open-entry .quick-open-entry-icon.dirty { - background-image: url('dirty-dark.svg'); -} \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts b/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts index c8cc3ddfc7..8e903e425a 100644 --- a/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts +++ b/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts @@ -726,7 +726,7 @@ export class EditorHistoryEntry extends EditorQuickOpenEntry { private resource: URI | undefined; private label: string; private description?: string; - private dirty: boolean; + private icon: string; constructor( input: IEditorInput | IResourceInput, @@ -747,22 +747,29 @@ export class EditorHistoryEntry extends EditorQuickOpenEntry { this.resource = resourceForEditorHistory(input, fileService); this.label = input.getName(); this.description = input.getDescription(); - this.dirty = input.isDirty(); + this.icon = this.getDirtyIndicatorForEditor(input); } else { const resourceInput = input as IResourceInput; this.resource = resourceInput.resource; this.label = resources.basenameOrAuthority(resourceInput.resource); this.description = labelService.getUriLabel(resources.dirname(this.resource), { relative: true }); - this.dirty = this.resource && this.textFileService.isDirty(this.resource); - - if (this.dirty && this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) { - this.dirty = false; // no dirty decoration if auto save is on with a short timeout - } + this.icon = this.getDirtyIndicatorForEditor(resourceInput); } } + private getDirtyIndicatorForEditor(input: EditorInput | IResourceInput): string { + let signalDirty = false; + if (input instanceof EditorInput) { + signalDirty = input.isDirty() && !input.isSaving(); + } else { + signalDirty = this.textFileService.isDirty(input.resource) && this.filesConfigurationService.getAutoSaveMode() !== AutoSaveMode.AFTER_SHORT_DELAY; + } + + return signalDirty ? 'codicon codicon-circle-filled' : ''; + } + getIcon(): string { - return this.dirty ? 'dirty' : ''; + return this.icon; } getLabel(): string { diff --git a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts index af7a7106d4..8209bbf06b 100644 --- a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts +++ b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts @@ -173,7 +173,7 @@ export class SidebarPart extends CompositePart implements IViewletServi const container = assertIsDefined(this.getContainer()); container.style.backgroundColor = this.getColor(SIDE_BAR_BACKGROUND) || ''; - container.style.color = this.getColor(SIDE_BAR_FOREGROUND); + container.style.color = this.getColor(SIDE_BAR_FOREGROUND) || ''; const borderColor = this.getColor(SIDE_BAR_BORDER) || this.getColor(contrastBorder); const isPositionLeft = this.layoutService.getSideBarPosition() === SideBarPosition.LEFT; diff --git a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css index 4ce813a644..9a7b13a40f 100644 --- a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css +++ b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css @@ -54,7 +54,7 @@ .monaco-workbench .part.statusbar > .items-container > .statusbar-item.has-beak:before { content: ''; position: absolute; - left: calc(50% - 9px); /* 3px (margin) + 5px (padding) + 1px (icon) = 9px */ + left: 10px; top: -5px; border-bottom-width: 5px; border-bottom-style: solid; diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 9e16b57eac..51b7e08a86 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -589,7 +589,7 @@ export class StatusbarPart extends Part implements IStatusbarService { // Background colors const backgroundColor = this.getColor(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? STATUS_BAR_BACKGROUND : STATUS_BAR_NO_FOLDER_BACKGROUND) || ''; container.style.backgroundColor = backgroundColor; - container.style.color = this.getColor(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? STATUS_BAR_FOREGROUND : STATUS_BAR_NO_FOLDER_FOREGROUND); + container.style.color = this.getColor(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? STATUS_BAR_FOREGROUND : STATUS_BAR_NO_FOLDER_FOREGROUND) || ''; // Border color const borderColor = this.getColor(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? STATUS_BAR_BORDER : STATUS_BAR_NO_FOLDER_BORDER) || this.getColor(contrastBorder); @@ -795,7 +795,7 @@ class StatusbarEntryItem extends Disposable { if (isBackground) { container.style.backgroundColor = colorResult || ''; } else { - container.style.color = colorResult; + container.style.color = colorResult || ''; } } diff --git a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts index bc3b5f7d66..81e67534c0 100644 --- a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts @@ -37,10 +37,11 @@ import { isFullscreen } from 'vs/base/browser/browser'; import { IHostService } from 'vs/workbench/services/host/browser/host'; // TODO@sbatten https://github.com/microsoft/vscode/issues/81360 -// tslint:disable-next-line: import-patterns layering TODO@sbatten +// eslint-disable-next-line code-layering, code-import-patterns import { IElectronService } from 'vs/platform/electron/node/electron'; import { optional } from 'vs/platform/instantiation/common/instantiation'; -// tslint:disable-next-line: import-patterns layering TODO@sbatten +// TODO@sbatten +// eslint-disable-next-line code-layering, code-import-patterns import { IElectronEnvironmentService } from 'vs/workbench/services/electron/electron-browser/electronEnvironmentService'; import { BrowserFeatures } from 'vs/base/browser/canIUse'; diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 97fac3d7f7..90fd7d6ac8 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -43,7 +43,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; // TODO@sbatten https://github.com/microsoft/vscode/issues/81360 -// tslint:disable-next-line: import-patterns layering +// eslint-disable-next-line code-layering, code-import-patterns import { IElectronService } from 'vs/platform/electron/node/electron'; export class TitlebarPart extends Part implements ITitleService { @@ -316,7 +316,7 @@ export class TitlebarPart extends Part implements ITitleService { const rootPath = root ? this.labelService.getUriLabel(root) : ''; const folderName = folder ? folder.name : ''; const folderPath = folder ? this.labelService.getUriLabel(folder.uri) : ''; - const dirty = editor?.isDirty() ? TitlebarPart.TITLE_DIRTY : ''; + const dirty = editor?.isDirty() && !editor.isSaving() ? TitlebarPart.TITLE_DIRTY : ''; const appName = this.productService.nameLong; const remoteName = this.labelService.getHostLabel(REMOTE_HOST_SCHEME, this.environmentService.configuration.remoteAuthority); const separator = TitlebarPart.TITLE_SEPARATOR; @@ -517,7 +517,7 @@ export class TitlebarPart extends Part implements ITitleService { } const titleForeground = this.getColor(this.isInactive ? TITLE_BAR_INACTIVE_FOREGROUND : TITLE_BAR_ACTIVE_FOREGROUND); - this.element.style.color = titleForeground; + this.element.style.color = titleForeground || ''; const titleBorder = this.getColor(TITLE_BAR_BORDER); this.element.style.borderBottom = titleBorder ? `1px solid ${titleBorder}` : ''; diff --git a/src/vs/workbench/browser/parts/views/customView.ts b/src/vs/workbench/browser/parts/views/customView.ts index 5c71f33d03..e39719b423 100644 --- a/src/vs/workbench/browser/parts/views/customView.ts +++ b/src/vs/workbench/browser/parts/views/customView.ts @@ -5,13 +5,13 @@ import 'vs/css!./media/views'; import { Event, Emitter } from 'vs/base/common/event'; -import { IDisposable, Disposable, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IAction, IActionViewItem, ActionRunner, Action } from 'vs/base/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; -import { ContextAwareMenuEntryActionViewItem, createAndFillInActionBarActions, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { ContextAwareMenuEntryActionViewItem, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ITreeView, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeViewDescriptor, IViewsRegistry, ViewContainer, ITreeItemLabel, Extensions } from 'vs/workbench/common/views'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; @@ -54,8 +54,9 @@ export class CustomTreeViewPane extends ViewPane { @IContextMenuService contextMenuService: IContextMenuService, @IConfigurationService configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService, + @IInstantiationService instantiationService: IInstantiationService, ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: options.title }, keybindingService, contextMenuService, configurationService, contextKeyService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: options.title, titleMenuId: MenuId.ViewTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, instantiationService); const { treeView } = (Registry.as(Extensions.ViewsRegistry).getView(options.id)); this.treeView = treeView; this._register(this.treeView.onDidChangeActions(() => this.updateActions(), this)); @@ -81,11 +82,11 @@ export class CustomTreeViewPane extends ViewPane { } getActions(): IAction[] { - return [...this.treeView.getPrimaryActions()]; + return [...super.getActions(), ...this.treeView.getPrimaryActions()]; } getSecondaryActions(): IAction[] { - return [...this.treeView.getSecondaryActions()]; + return [...super.getSecondaryActions(), ...this.treeView.getSecondaryActions()]; } getActionViewItem(action: IAction): IActionViewItem | undefined { @@ -101,51 +102,6 @@ export class CustomTreeViewPane extends ViewPane { } } -class TitleMenus extends Disposable { - - private titleActions: IAction[] = []; - private readonly titleActionsDisposable = this._register(new MutableDisposable()); - private titleSecondaryActions: IAction[] = []; - - private _onDidChangeTitle = this._register(new Emitter()); - readonly onDidChangeTitle: Event = this._onDidChangeTitle.event; - - constructor( - id: string, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IMenuService private readonly menuService: IMenuService, - ) { - super(); - - const scopedContextKeyService = this._register(this.contextKeyService.createScoped()); - scopedContextKeyService.createKey('view', id); - - const titleMenu = this._register(this.menuService.createMenu(MenuId.ViewTitle, scopedContextKeyService)); - const updateActions = () => { - this.titleActions = []; - this.titleSecondaryActions = []; - this.titleActionsDisposable.value = createAndFillInActionBarActions(titleMenu, undefined, { primary: this.titleActions, secondary: this.titleSecondaryActions }); - this._onDidChangeTitle.fire(); - }; - - this._register(titleMenu.onDidChange(updateActions)); - updateActions(); - - this._register(toDisposable(() => { - this.titleActions = []; - this.titleSecondaryActions = []; - })); - } - - getTitleActions(): IAction[] { - return this.titleActions; - } - - getTitleSecondaryActions(): IAction[] { - return this.titleSecondaryActions; - } -} - class Root implements ITreeItem { label = { label: 'root' }; handle = '0'; @@ -175,7 +131,6 @@ export class CustomTreeView extends Disposable implements ITreeView { private root: ITreeItem; private elementsToRefresh: ITreeItem[] = []; - private menus: TitleMenus; private readonly _onDidExpandItem: Emitter = this._register(new Emitter()); readonly onDidExpandItem: Event = this._onDidExpandItem.event; @@ -211,8 +166,6 @@ export class CustomTreeView extends Disposable implements ITreeView { ) { super(); this.root = new Root(); - this.menus = this._register(instantiationService.createInstance(TitleMenus, this.id)); - this._register(this.menus.onDidChangeTitle(() => this._onDidChangeActions.fire())); this._register(this.themeService.onDidFileIconThemeChange(() => this.doRefresh([this.root]) /** soft refresh **/)); this._register(this.themeService.onThemeChange(() => this.doRefresh([this.root]) /** soft refresh **/)); this._register(this.configurationService.onDidChangeConfiguration(e => { @@ -309,15 +262,14 @@ export class CustomTreeView extends Disposable implements ITreeView { getPrimaryActions(): IAction[] { if (this.showCollapseAllAction) { - const collapseAllAction = new Action('vs.tree.collapse', localize('collapseAll', "Collapse All"), 'monaco-tree-action codicon-collapse-all', true, () => this.tree ? new CollapseAllAction(this.tree, true).run() : Promise.resolve()); - return [...this.menus.getTitleActions(), collapseAllAction]; + return [new Action('vs.tree.collapse', localize('collapseAll', "Collapse All"), 'monaco-tree-action codicon-collapse-all', true, () => this.tree ? new CollapseAllAction(this.tree, true).run() : Promise.resolve())]; } else { - return this.menus.getTitleActions(); + return []; } } getSecondaryActions(): IAction[] { - return this.menus.getTitleSecondaryActions(); + return []; } setVisibility(isVisible: boolean): void { @@ -357,6 +309,8 @@ export class CustomTreeView extends Disposable implements ITreeView { // Pass Focus to Viewer this.tree.domFocus(); + } else if (this.tree) { + this.tree.domFocus(); } else { this.domNode.focus(); } diff --git a/src/vs/workbench/browser/parts/views/media/views.css b/src/vs/workbench/browser/parts/views/media/views.css index d76397a06a..1afd8230dc 100644 --- a/src/vs/workbench/browser/parts/views/media/views.css +++ b/src/vs/workbench/browser/parts/views/media/views.css @@ -121,7 +121,7 @@ margin-top: 3px; } -.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item .custom-view-tree-node-item-resourceLabel .monaco-icon-label-description-container { +.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item .custom-view-tree-node-item-resourceLabel .monaco-icon-label-container > .monaco-icon-name-container { flex: 1; } diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index b004a4a0da..ebef5db465 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -10,7 +10,7 @@ import { ColorIdentifier } from 'vs/platform/theme/common/colorRegistry'; import { attachStyler, IColorMapping } from 'vs/platform/theme/common/styler'; import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND, SIDE_BAR_SECTION_HEADER_FOREGROUND, SIDE_BAR_SECTION_HEADER_BACKGROUND, SIDE_BAR_SECTION_HEADER_BORDER } from 'vs/workbench/common/theme'; import { append, $, trackFocus, toggleClass, EventType, isAncestor, Dimension, addDisposableListener } from 'vs/base/browser/dom'; -import { IDisposable, combinedDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, combinedDisposable, dispose, toDisposable, Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { firstIndex } from 'vs/base/common/arrays'; import { IAction, IActionRunner, ActionRunner } from 'vs/base/common/actions'; import { IActionViewItem, ActionsOrientation, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -36,6 +36,8 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IViewPaneContainer } from 'vs/workbench/common/viewPaneContainer'; import { Component } from 'vs/workbench/common/component'; +import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { createAndFillInActionBarActions, ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; export interface IPaneColors extends IColorMapping { dropBackground?: ColorIdentifier; @@ -49,6 +51,7 @@ export interface IViewPaneOptions extends IPaneOptions { id: string; title: string; showActionsAlways?: boolean; + titleMenuId?: MenuId; } export abstract class ViewPane extends Pane implements IView { @@ -73,6 +76,8 @@ export abstract class ViewPane extends Pane implements IView { readonly id: string; title: string; + private readonly menuActions?: ViewMenuActions; + protected actionRunner?: IActionRunner; protected toolbar?: ToolBar; private readonly showActionsAlways: boolean = false; @@ -85,7 +90,8 @@ 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 contextKeyService: IContextKeyService, + @IInstantiationService protected instantiationService: IInstantiationService, ) { super(options); @@ -94,6 +100,11 @@ export abstract class ViewPane extends Pane implements IView { this.actionRunner = options.actionRunner; this.showActionsAlways = !!options.showActionsAlways; this.focusedViewContextKey = FocusedViewContext.bindTo(contextKeyService); + + if (options.titleMenuId !== undefined) { + this.menuActions = this._register(instantiationService.createInstance(ViewMenuActions, this.id, options.titleMenuId)); + this._register(this.menuActions.onDidChangeTitle(() => this.updateActions())); + } } setVisible(visible: boolean): void { @@ -207,14 +218,17 @@ export abstract class ViewPane extends Pane implements IView { } getActions(): IAction[] { - return []; + return this.menuActions ? this.menuActions.getPrimaryActions() : []; } getSecondaryActions(): IAction[] { - return []; + return this.menuActions ? this.menuActions.getSecondaryActions() : []; } getActionViewItem(action: IAction): IActionViewItem | undefined { + if (action instanceof MenuItemAction) { + return this.instantiationService.createInstance(ContextAwareMenuEntryActionViewItem, action); + } return undefined; } @@ -316,7 +330,6 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } create(parent: HTMLElement): void { - // super.create(parent); this.paneview = this._register(new PaneView(parent, this.options)); this._register(this.paneview.onDidDrop(({ from, to }) => this.movePane(from as ViewPane, to as ViewPane))); this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, (e: MouseEvent) => this.showContextMenu(new StandardMouseEvent(e)))); @@ -493,7 +506,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } protected createView(viewDescriptor: IViewDescriptor, options: IViewletViewOptions): ViewPane { - return (this.instantiationService as any).createInstance(viewDescriptor.ctorDescriptor.ctor, ...(viewDescriptor.ctorDescriptor.arguments || []), options) as ViewPane; + return (this.instantiationService as any).createInstance(viewDescriptor.ctorDescriptor.ctor, ...(viewDescriptor.ctorDescriptor.staticArguments || []), options) as ViewPane; } getView(id: string): ViewPane | undefined { @@ -760,3 +773,48 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } +class ViewMenuActions extends Disposable { + + private primaryActions: IAction[] = []; + private readonly titleActionsDisposable = this._register(new MutableDisposable()); + private secondaryActions: IAction[] = []; + + private _onDidChangeTitle = this._register(new Emitter()); + readonly onDidChangeTitle: Event = this._onDidChangeTitle.event; + + constructor( + viewId: string, + menuId: MenuId, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IMenuService private readonly menuService: IMenuService, + ) { + super(); + + const scopedContextKeyService = this._register(this.contextKeyService.createScoped()); + scopedContextKeyService.createKey('view', viewId); + + const menu = this._register(this.menuService.createMenu(menuId, scopedContextKeyService)); + const updateActions = () => { + this.primaryActions = []; + this.secondaryActions = []; + this.titleActionsDisposable.value = createAndFillInActionBarActions(menu, undefined, { primary: this.primaryActions, secondary: this.secondaryActions }); + this._onDidChangeTitle.fire(); + }; + + this._register(menu.onDidChange(updateActions)); + updateActions(); + + this._register(toDisposable(() => { + this.primaryActions = []; + this.secondaryActions = []; + })); + } + + getPrimaryActions(): IAction[] { + return this.primaryActions; + } + + getSecondaryActions(): IAction[] { + return this.secondaryActions; + } +} diff --git a/src/vs/workbench/browser/parts/views/views.ts b/src/vs/workbench/browser/parts/views/views.ts index 8bf4525295..eb4be25182 100644 --- a/src/vs/workbench/browser/parts/views/views.ts +++ b/src/vs/workbench/browser/parts/views/views.ts @@ -5,13 +5,13 @@ import 'vs/css!./media/views'; import { Disposable, IDisposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { IViewsService, IViewsViewlet, ViewContainer, IViewDescriptor, IViewContainersRegistry, Extensions as ViewExtensions, IView, IViewDescriptorCollection, IViewsRegistry } from 'vs/workbench/common/views'; +import { IViewDescriptorService, ViewContainer, IViewDescriptor, IViewContainersRegistry, Extensions as ViewExtensions, IView, IViewDescriptorCollection, IViewsRegistry, ViewContainerLocation, IViewsService } from 'vs/workbench/common/views'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { IStorageService, StorageScope, IWorkspaceStorageChangeEvent } from 'vs/platform/storage/common/storage'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IContextKeyService, IContextKeyChangeEvent, IReadableSet, IContextKey, RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { Event, Emitter } from 'vs/base/common/event'; -import { firstIndex, move, isNonEmptyArray } from 'vs/base/common/arrays'; +import { firstIndex, move } from 'vs/base/common/arrays'; import { isUndefinedOrNull, isUndefined } from 'vs/base/common/types'; import { MenuId, MenuRegistry, ICommandAction } from 'vs/platform/actions/common/actions'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; @@ -21,20 +21,8 @@ import { values } from 'vs/base/common/map'; import { IFileIconTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { toggleClass, addClass } from 'vs/base/browser/dom'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; - -function filterViewRegisterEvent(container: ViewContainer, event: Event<{ viewContainer: ViewContainer, views: IViewDescriptor[]; }>): Event { - return Event.chain(event) - .map(({ views, viewContainer }) => viewContainer === container ? views : []) - .filter(views => views.length > 0) - .event; -} - -function filterViewMoveEvent(container: ViewContainer, event: Event<{ from: ViewContainer, to: ViewContainer, views: IViewDescriptor[]; }>): Event<{ added?: IViewDescriptor[], removed?: IViewDescriptor[]; }> { - return Event.chain(event) - .map(({ views, from, to }) => from === container ? { removed: views } : to === container ? { added: views } : {}) - .filter(({ added, removed }) => isNonEmptyArray(added) || isNonEmptyArray(removed)) - .event; -} +import { IPaneComposite } from 'vs/workbench/common/panecomposite'; +import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; class CounterSet implements IReadableSet { @@ -78,8 +66,11 @@ class ViewDescriptorCollection extends Disposable implements IViewDescriptorColl private contextKeys = new CounterSet(); private items: IViewItem[] = []; - private _onDidChange: Emitter<{ added: IViewDescriptor[], removed: IViewDescriptor[]; }> = this._register(new Emitter<{ added: IViewDescriptor[], removed: IViewDescriptor[]; }>()); - readonly onDidChangeActiveViews: Event<{ added: IViewDescriptor[], removed: IViewDescriptor[]; }> = this._onDidChange.event; + private _onDidChangeViews: Emitter<{ added: IViewDescriptor[], removed: IViewDescriptor[]; }> = this._register(new Emitter<{ added: IViewDescriptor[], removed: IViewDescriptor[]; }>()); + readonly onDidChangeViews: Event<{ added: IViewDescriptor[], removed: IViewDescriptor[]; }> = this._onDidChangeViews.event; + + private _onDidChangeActiveViews: Emitter<{ added: IViewDescriptor[], removed: IViewDescriptor[]; }> = this._register(new Emitter<{ added: IViewDescriptor[], removed: IViewDescriptor[]; }>()); + readonly onDidChangeActiveViews: Event<{ added: IViewDescriptor[], removed: IViewDescriptor[]; }> = this._onDidChangeActiveViews.event; get activeViewDescriptors(): IViewDescriptor[] { return this.items @@ -92,34 +83,13 @@ class ViewDescriptorCollection extends Disposable implements IViewDescriptorColl } constructor( - container: ViewContainer, - @IContextKeyService private readonly contextKeyService: IContextKeyService + @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { super(); - const viewsRegistry = Registry.as(ViewExtensions.ViewsRegistry); - const onRelevantViewsRegistered = filterViewRegisterEvent(container, viewsRegistry.onViewsRegistered); - this._register(onRelevantViewsRegistered(this.onViewsRegistered, this)); - - const onRelevantViewsMoved = filterViewMoveEvent(container, viewsRegistry.onDidChangeContainer); - this._register(onRelevantViewsMoved(({ added, removed }) => { - if (isNonEmptyArray(added)) { - this.onViewsRegistered(added); - } - if (isNonEmptyArray(removed)) { - this.onViewsDeregistered(removed); - } - })); - - const onRelevantViewsDeregistered = filterViewRegisterEvent(container, viewsRegistry.onViewsDeregistered); - this._register(onRelevantViewsDeregistered(this.onViewsDeregistered, this)); - - const onRelevantContextChange = Event.filter(contextKeyService.onDidChangeContext, e => e.affectsSome(this.contextKeys)); - this._register(onRelevantContextChange(this.onContextChanged, this)); - - this.onViewsRegistered(viewsRegistry.getViews(container)); + this._register(Event.filter(contextKeyService.onDidChangeContext, e => e.affectsSome(this.contextKeys))(this.onContextChanged, this)); } - private onViewsRegistered(viewDescriptors: IViewDescriptor[]): void { + addViews(viewDescriptors: IViewDescriptor[]): void { const added: IViewDescriptor[] = []; for (const viewDescriptor of viewDescriptors) { @@ -141,12 +111,14 @@ class ViewDescriptorCollection extends Disposable implements IViewDescriptorColl } } + this._onDidChangeViews.fire({ added: viewDescriptors, removed: [] }); + if (added.length) { - this._onDidChange.fire({ added, removed: [] }); + this._onDidChangeActiveViews.fire({ added, removed: [] }); } } - private onViewsDeregistered(viewDescriptors: IViewDescriptor[]): void { + removeViews(viewDescriptors: IViewDescriptor[]): void { const removed: IViewDescriptor[] = []; for (const viewDescriptor of viewDescriptors) { @@ -170,8 +142,10 @@ class ViewDescriptorCollection extends Disposable implements IViewDescriptorColl } } + this._onDidChangeViews.fire({ added: [], removed: viewDescriptors }); + if (removed.length) { - this._onDidChange.fire({ added: [], removed }); + this._onDidChangeActiveViews.fire({ added: [], removed }); } } @@ -194,7 +168,7 @@ class ViewDescriptorCollection extends Disposable implements IViewDescriptorColl } if (added.length || removed.length) { - this._onDidChange.fire({ added, removed }); + this._onDidChangeActiveViews.fire({ added, removed }); } } @@ -249,16 +223,13 @@ export class ContributableViewsModel extends Disposable { constructor( container: ViewContainer, - viewsService: IViewsService, + viewsService: IViewDescriptorService, protected viewStates = new Map(), ) { super(); const viewDescriptorCollection = viewsService.getViewDescriptors(container); - - if (viewDescriptorCollection) { - this._register(viewDescriptorCollection.onDidChangeActiveViews(() => this.onDidChangeViewDescriptors(viewDescriptorCollection.activeViewDescriptors))); - this.onDidChangeViewDescriptors(viewDescriptorCollection.activeViewDescriptors); - } + this._register(viewDescriptorCollection.onDidChangeActiveViews(() => this.onDidChangeViewDescriptors(viewDescriptorCollection.activeViewDescriptors))); + this.onDidChangeViewDescriptors(viewDescriptorCollection.activeViewDescriptors); } isVisible(id: string): boolean { @@ -487,13 +458,13 @@ export class PersistentContributableViewsModel extends ContributableViewsModel { constructor( container: ViewContainer, viewletStateStorageId: string, - @IViewsService viewsService: IViewsService, + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IStorageService storageService: IStorageService, ) { const globalViewsStateStorageId = `${viewletStateStorageId}.hidden`; const viewStates = PersistentContributableViewsModel.loadViewsStates(viewletStateStorageId, globalViewsStateStorageId, storageService); - super(container, viewsService, viewStates); + super(container, viewDescriptorService, viewStates); this.workspaceViewsStateStorageId = viewletStateStorageId; this.globalViewsStateStorageId = globalViewsStateStorageId; @@ -628,92 +599,50 @@ export class ViewsService extends Disposable implements IViewsService { _serviceBrand: undefined; - private readonly viewDescriptorCollections: Map; + private readonly viewContainersRegistry: IViewContainersRegistry; private readonly viewDisposable: Map; - private readonly activeViewContextKeys: Map>; constructor( - @IViewletService private readonly viewletService: IViewletService, - @IContextKeyService private readonly contextKeyService: IContextKeyService + @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, + @IPanelService private readonly panelService: IPanelService, + @IViewletService private readonly viewletService: IViewletService ) { super(); - this.viewDescriptorCollections = new Map(); + this.viewContainersRegistry = Registry.as(ViewExtensions.ViewContainersRegistry); this.viewDisposable = new Map(); - this.activeViewContextKeys = new Map>(); - const viewContainersRegistry = Registry.as(ViewExtensions.ViewContainersRegistry); - const viewsRegistry = Registry.as(ViewExtensions.ViewsRegistry); - viewContainersRegistry.all.forEach(viewContainer => { - this.onDidRegisterViews(viewContainer, viewsRegistry.getViews(viewContainer)); - this.onDidRegisterViewContainer(viewContainer); - }); - this._register(viewsRegistry.onViewsRegistered(({ views, viewContainer }) => this.onDidRegisterViews(viewContainer, views))); - this._register(viewsRegistry.onViewsDeregistered(({ views }) => this.onDidDeregisterViews(views))); - this._register(viewsRegistry.onDidChangeContainer(({ views, to }) => { this.onDidDeregisterViews(views); this.onDidRegisterViews(to, views); })); this._register(toDisposable(() => { this.viewDisposable.forEach(disposable => disposable.dispose()); this.viewDisposable.clear(); })); - this._register(viewContainersRegistry.onDidRegister(({ viewContainer }) => this.onDidRegisterViewContainer(viewContainer))); - this._register(viewContainersRegistry.onDidDeregister(({ viewContainer }) => this.onDidDeregisterViewContainer(viewContainer))); - this._register(toDisposable(() => { - this.viewDescriptorCollections.forEach(({ disposable }) => disposable.dispose()); - this.viewDescriptorCollections.clear(); + + this.viewContainersRegistry.all.forEach(viewContainer => this.onViewContainerRegistered(viewContainer)); + this._register(this.viewContainersRegistry.onDidRegister(({ viewContainer }) => this.onViewContainerRegistered(viewContainer))); + } + + private onViewContainerRegistered(viewContainer: ViewContainer): void { + const viewDescriptorCollection = this.viewDescriptorService.getViewDescriptors(viewContainer); + this.onViewsRegistered(viewDescriptorCollection.allViewDescriptors, viewContainer); + this._register(viewDescriptorCollection.onDidChangeViews(({ added, removed }) => { + this.onViewsRegistered(added, viewContainer); + this.onViewsDeregistered(removed, viewContainer); })); } - getViewDescriptors(container: ViewContainer): IViewDescriptorCollection | null { - const viewDescriptorCollectionItem = this.viewDescriptorCollections.get(container); - return viewDescriptorCollectionItem ? viewDescriptorCollectionItem.viewDescriptorCollection : null; - } - - async openView(id: string, focus: boolean): Promise { - const viewContainer = Registry.as(ViewExtensions.ViewsRegistry).getViewContainer(id); - if (viewContainer) { - const viewletDescriptor = this.viewletService.getViewlet(viewContainer.id); - if (viewletDescriptor) { - const viewlet = await this.viewletService.openViewlet(viewletDescriptor.id, focus) as IViewsViewlet | null; - if (viewlet && viewlet.openView) { - return viewlet.openView(id, focus); - } - } + private onViewsRegistered(views: IViewDescriptor[], container: ViewContainer): void { + const location = this.viewContainersRegistry.getViewContainerLocation(container); + if (location === undefined) { + return; } - return null; - } - - private onDidRegisterViewContainer(viewContainer: ViewContainer): void { - const disposables = new DisposableStore(); - const viewDescriptorCollection = disposables.add(new ViewDescriptorCollection(viewContainer, this.contextKeyService)); - - this.onDidChangeActiveViews({ added: viewDescriptorCollection.activeViewDescriptors, removed: [] }); - viewDescriptorCollection.onDidChangeActiveViews(changed => this.onDidChangeActiveViews(changed), this, disposables); - - this.viewDescriptorCollections.set(viewContainer, { viewDescriptorCollection, disposable: disposables }); - } - - private onDidDeregisterViewContainer(viewContainer: ViewContainer): void { - const viewDescriptorCollectionItem = this.viewDescriptorCollections.get(viewContainer); - if (viewDescriptorCollectionItem) { - viewDescriptorCollectionItem.disposable.dispose(); - this.viewDescriptorCollections.delete(viewContainer); - } - } - - private onDidChangeActiveViews({ added, removed }: { added: IViewDescriptor[], removed: IViewDescriptor[]; }): void { - added.forEach(viewDescriptor => this.getOrCreateActiveViewContextKey(viewDescriptor).set(true)); - removed.forEach(viewDescriptor => this.getOrCreateActiveViewContextKey(viewDescriptor).set(false)); - } - - private onDidRegisterViews(container: ViewContainer, views: IViewDescriptor[]): void { - const viewlet = this.viewletService.getViewlet(container.id); + const composite = this.getComposite(container.id, location); for (const viewDescriptor of views) { const disposables = new DisposableStore(); const command: ICommandAction = { id: viewDescriptor.focusCommand ? viewDescriptor.focusCommand.id : `${viewDescriptor.id}.focus`, title: { original: `Focus on ${viewDescriptor.name} View`, value: localize('focus view', "Focus on {0} View", viewDescriptor.name) }, - category: viewlet ? viewlet.name : localize('view category', "View"), + category: composite ? composite.name : localize('view category', "View"), }; const when = ContextKeyExpr.has(`${viewDescriptor.id}.active`); @@ -741,7 +670,7 @@ export class ViewsService extends Disposable implements IViewsService { } } - private onDidDeregisterViews(views: IViewDescriptor[]): void { + private onViewsDeregistered(views: IViewDescriptor[], container: ViewContainer): void { for (const view of views) { const disposable = this.viewDisposable.get(view); if (disposable) { @@ -751,6 +680,308 @@ export class ViewsService extends Disposable implements IViewsService { } } + private async openComposite(compositeId: string, location: ViewContainerLocation, focus?: boolean): Promise { + if (location === ViewContainerLocation.Sidebar) { + return this.viewletService.openViewlet(compositeId, focus); + } else if (location === ViewContainerLocation.Panel) { + return this.panelService.openPanel(compositeId, focus) as IPaneComposite; + } + return undefined; + } + + private getComposite(compositeId: string, location: ViewContainerLocation): { id: string, name: string } | undefined { + if (location === ViewContainerLocation.Sidebar) { + return this.viewletService.getViewlet(compositeId); + } else if (location === ViewContainerLocation.Panel) { + return this.panelService.getPanel(compositeId); + } + + return undefined; + } + + async openView(id: string, focus: boolean): Promise { + const viewContainer = this.viewDescriptorService.getViewContainer(id); + if (viewContainer) { + const location = this.viewContainersRegistry.getViewContainerLocation(viewContainer); + const compositeDescriptor = this.getComposite(viewContainer.id, location!); + if (compositeDescriptor) { + const paneComposite = await this.openComposite(compositeDescriptor.id, location!, focus) as IPaneComposite | undefined; + if (paneComposite && paneComposite.openView) { + return paneComposite.openView(id, focus); + } + } + } + + return null; + } +} + +export class ViewDescriptorService extends Disposable implements IViewDescriptorService { + + _serviceBrand: undefined; + + private static readonly CACHED_VIEW_POSITIONS = 'views.cachedViewPositions'; + + private readonly viewDescriptorCollections: Map; + private readonly activeViewContextKeys: Map>; + + private readonly viewsRegistry: IViewsRegistry; + private readonly viewContainersRegistry: IViewContainersRegistry; + + private cachedViewToContainer: Map; + + + private _cachedViewPositionsValue: string | undefined; + private get cachedViewPositionsValue(): string { + if (!this._cachedViewPositionsValue) { + this._cachedViewPositionsValue = this.getStoredCachedViewPositionsValue(); + } + + return this._cachedViewPositionsValue; + } + + private set cachedViewPositionsValue(value: string) { + if (this.cachedViewPositionsValue !== value) { + this._cachedViewPositionsValue = value; + this.setStoredCachedViewPositionsValue(value); + } + } + + constructor( + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IStorageService private readonly storageService: IStorageService + ) { + super(); + + this.viewDescriptorCollections = new Map(); + this.activeViewContextKeys = new Map>(); + + this.viewContainersRegistry = Registry.as(ViewExtensions.ViewContainersRegistry); + this.viewsRegistry = Registry.as(ViewExtensions.ViewsRegistry); + + this.cachedViewToContainer = this.getCachedViewPositions(); + + // Register all containers that were registered before this ctor + this.viewContainersRegistry.all.forEach(viewContainer => this.onDidRegisterViewContainer(viewContainer)); + + this._register(this.viewsRegistry.onViewsRegistered(({ views, viewContainer }) => this.onDidRegisterViews(views, viewContainer))); + this._register(this.viewsRegistry.onViewsDeregistered(({ views, viewContainer }) => this.onDidDeregisterViews(views, viewContainer))); + + this._register(this.viewsRegistry.onDidChangeContainer(({ views, from, to }) => { this.removeViews(from, views); this.addViews(to, views); })); + + this._register(this.viewContainersRegistry.onDidRegister(({ viewContainer }) => this.onDidRegisterViewContainer(viewContainer))); + this._register(this.viewContainersRegistry.onDidDeregister(({ viewContainer }) => this.onDidDeregisterViewContainer(viewContainer))); + this._register(toDisposable(() => { + this.viewDescriptorCollections.forEach(({ disposable }) => disposable.dispose()); + this.viewDescriptorCollections.clear(); + })); + + this._register(this.storageService.onDidChangeStorage((e) => { this.onDidStorageChange(e); })); + } + + private registerGroupedViews(groupedViews: Map): void { + // Register views that have already been registered to their correct view containers + for (const viewContainerId of groupedViews.keys()) { + const viewContainer = this.viewContainersRegistry.get(viewContainerId); + + // The container has not been registered yet + if (!viewContainer || !this.viewDescriptorCollections.has(viewContainer)) { + continue; + } + + this.addViews(viewContainer, groupedViews.get(viewContainerId)!); + } + } + + private deregisterGroupedViews(groupedViews: Map): void { + // Register views that have already been registered to their correct view containers + for (const viewContainerId of groupedViews.keys()) { + const viewContainer = this.viewContainersRegistry.get(viewContainerId); + + // The container has not been registered yet + if (!viewContainer || !this.viewDescriptorCollections.has(viewContainer)) { + continue; + } + + this.removeViews(viewContainer, groupedViews.get(viewContainerId)!); + } + } + + private onDidRegisterViews(views: IViewDescriptor[], viewContainer: ViewContainer): void { + // When views are registered, we need to regroup them based on the cache + const regroupedViews = this.regroupViews(viewContainer.id, views); + + // Once they are grouped, try registering them which occurs + // if the container has already been registered within this service + this.registerGroupedViews(regroupedViews); + } + + private onDidDeregisterViews(views: IViewDescriptor[], viewContainer: ViewContainer): void { + // When views are registered, we need to regroup them based on the cache + const regroupedViews = this.regroupViews(viewContainer.id, views); + this.deregisterGroupedViews(regroupedViews); + } + + private regroupViews(containerId: string, views: IViewDescriptor[]): Map { + const ret = new Map(); + + views.forEach(viewDescriptor => { + const cachedContainerId = this.cachedViewToContainer.get(viewDescriptor.id); + const correctContainerId = cachedContainerId || containerId; + + const containerViews = ret.get(correctContainerId) || []; + containerViews.push(viewDescriptor); + ret.set(correctContainerId, containerViews); + }); + + return ret; + } + + getViewContainer(viewId: string): ViewContainer | null { + const containerId = this.cachedViewToContainer.get(viewId); + return containerId ? + this.viewContainersRegistry.get(containerId) ?? null : + this.viewsRegistry.getViewContainer(viewId); + } + + getViewDescriptors(container: ViewContainer): ViewDescriptorCollection { + return this.getOrRegisterViewDescriptorCollection(container); + } + + moveViews(views: IViewDescriptor[], viewContainer: ViewContainer): void { + if (!views.length) { + return; + } + + const from = this.getViewContainer(views[0].id); + const to = viewContainer; + + if (from && to && from !== to) { + this.removeViews(from, views); + this.addViews(to, views); + this.saveViewPositionsToCache(); + } + } + + private getCachedViewPositions(): Map { + return new Map(JSON.parse(this.cachedViewPositionsValue)); + } + + private onDidStorageChange(e: IWorkspaceStorageChangeEvent): void { + if (e.key === ViewDescriptorService.CACHED_VIEW_POSITIONS && e.scope === StorageScope.GLOBAL + && this.cachedViewPositionsValue !== this.getStoredCachedViewPositionsValue() /* This checks if current window changed the value or not */) { + this._cachedViewPositionsValue = this.getStoredCachedViewPositionsValue(); + + const newCachedPositions = this.getCachedViewPositions(); + + for (let viewId of newCachedPositions.keys()) { + const prevViewContainer = this.getViewContainer(viewId); + const newViewContainer = this.viewContainersRegistry.get(newCachedPositions.get(viewId)!); + if (prevViewContainer && newViewContainer && newViewContainer !== prevViewContainer) { + const viewDescriptor = this.viewsRegistry.getView(viewId); + if (viewDescriptor) { + // We don't call move views to avoid sending intermediate + // cached data to the window that gave us this information + this.removeViews(prevViewContainer, [viewDescriptor]); + this.addViews(newViewContainer, [viewDescriptor]); + } + } + } + + this.cachedViewToContainer = this.getCachedViewPositions(); + } + } + + private getStoredCachedViewPositionsValue(): string { + return this.storageService.get(ViewDescriptorService.CACHED_VIEW_POSITIONS, StorageScope.GLOBAL, '[]'); + } + + private setStoredCachedViewPositionsValue(value: string): void { + this.storageService.store(ViewDescriptorService.CACHED_VIEW_POSITIONS, value, StorageScope.GLOBAL); + } + + private saveViewPositionsToCache(): void { + this.viewContainersRegistry.all.forEach(viewContainer => { + const viewDescriptorCollection = this.getViewDescriptors(viewContainer); + viewDescriptorCollection.allViewDescriptors.forEach(viewDescriptor => { + this.cachedViewToContainer.set(viewDescriptor.id, viewContainer.id); + }); + }); + + this.cachedViewPositionsValue = JSON.stringify([...this.cachedViewToContainer]); + } + + private getViewsByContainer(viewContainer: ViewContainer): IViewDescriptor[] { + const result = this.viewsRegistry.getViews(viewContainer).filter(viewDescriptor => { + const cachedContainer = this.cachedViewToContainer.get(viewDescriptor.id) || viewContainer.id; + return cachedContainer === viewContainer.id; + }); + + for (const [viewId, containerId] of this.cachedViewToContainer.entries()) { + if (containerId !== viewContainer.id) { + continue; + } + + if (this.viewsRegistry.getViewContainer(viewId) === viewContainer) { + continue; + } + + const viewDescriptor = this.viewsRegistry.getView(viewId); + if (viewDescriptor) { + result.push(viewDescriptor); + } + } + + return result; + } + + private onDidRegisterViewContainer(viewContainer: ViewContainer): void { + this.getOrRegisterViewDescriptorCollection(viewContainer); + } + + private getOrRegisterViewDescriptorCollection(viewContainer: ViewContainer): ViewDescriptorCollection { + let viewDescriptorCollection = this.viewDescriptorCollections.get(viewContainer)?.viewDescriptorCollection; + + if (!viewDescriptorCollection) { + const disposables = new DisposableStore(); + viewDescriptorCollection = disposables.add(new ViewDescriptorCollection(this.contextKeyService)); + + this.onDidChangeActiveViews({ added: viewDescriptorCollection.activeViewDescriptors, removed: [] }); + viewDescriptorCollection.onDidChangeActiveViews(changed => this.onDidChangeActiveViews(changed), this, disposables); + + this.viewDescriptorCollections.set(viewContainer, { viewDescriptorCollection, disposable: disposables }); + + const viewsToRegister = this.getViewsByContainer(viewContainer); + if (viewsToRegister.length) { + this.addViews(viewContainer, viewsToRegister); + } + } + + return viewDescriptorCollection; + } + + private onDidDeregisterViewContainer(viewContainer: ViewContainer): void { + const viewDescriptorCollectionItem = this.viewDescriptorCollections.get(viewContainer); + if (viewDescriptorCollectionItem) { + viewDescriptorCollectionItem.disposable.dispose(); + this.viewDescriptorCollections.delete(viewContainer); + } + } + + private onDidChangeActiveViews({ added, removed }: { added: IViewDescriptor[], removed: IViewDescriptor[]; }): void { + added.forEach(viewDescriptor => this.getOrCreateActiveViewContextKey(viewDescriptor).set(true)); + removed.forEach(viewDescriptor => this.getOrCreateActiveViewContextKey(viewDescriptor).set(false)); + } + + private addViews(container: ViewContainer, views: IViewDescriptor[]): void { + this.getViewDescriptors(container).addViews(views); + } + + private removeViews(container: ViewContainer, views: IViewDescriptor[]): void { + const viewDescriptorCollection = this.getViewDescriptors(container); + viewDescriptorCollection.removeViews(views); + } + private getOrCreateActiveViewContextKey(viewDescriptor: IViewDescriptor): IContextKey { const activeContextKeyId = `${viewDescriptor.id}.active`; let contextKey = this.activeViewContextKeys.get(activeContextKeyId); @@ -775,4 +1006,5 @@ export function createFileIconThemableTreeContainerScope(container: HTMLElement, return themeService.onDidFileIconThemeChange(onDidChangeFileIconTheme); } +registerSingleton(IViewDescriptorService, ViewDescriptorService); registerSingleton(IViewsService, ViewsService); diff --git a/src/vs/workbench/browser/parts/views/viewsViewlet.ts b/src/vs/workbench/browser/parts/views/viewsViewlet.ts index 60b0ba90bb..53f2b5fd17 100644 --- a/src/vs/workbench/browser/parts/views/viewsViewlet.ts +++ b/src/vs/workbench/browser/parts/views/viewsViewlet.ts @@ -30,11 +30,11 @@ export interface IViewletViewOptions extends IViewPaneOptions { export abstract class FilterViewPaneContainer extends ViewPaneContainer { private constantViewDescriptors: Map = new Map(); private allViews: Map> = new Map(); - private filterValue: string | undefined; + private filterValue: string[] | undefined; constructor( viewletId: string, - onDidChangeFilterValue: Event, + onDidChangeFilterValue: Event, @IConfigurationService configurationService: IConfigurationService, @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @ITelemetryService telemetryService: ITelemetryService, @@ -67,7 +67,7 @@ export abstract class FilterViewPaneContainer extends ViewPaneContainer { this.allViews.set(filterOnValue, new Map()); } this.allViews.get(filterOnValue)!.set(descriptor.id, descriptor); - if (filterOnValue !== this.filterValue) { + if (this.filterValue && !this.filterValue.includes(filterOnValue)) { this.viewsModel.setVisible(descriptor.id, false); } }); @@ -79,7 +79,7 @@ export abstract class FilterViewPaneContainer extends ViewPaneContainer { protected abstract getFilterOn(viewDescriptor: IViewDescriptor): string | undefined; - private onFilterChanged(newFilterValue: string) { + private onFilterChanged(newFilterValue: string[]) { if (this.allViews.size === 0) { this.updateAllViews(this.viewsModel.viewDescriptors); } @@ -107,18 +107,32 @@ export abstract class FilterViewPaneContainer extends ViewPaneContainer { return result; } - private getViewsForTarget(target: string): IViewDescriptor[] { - return this.allViews.has(target) ? Array.from(this.allViews.get(target)!.values()) : []; + private getViewsForTarget(target: string[]): IViewDescriptor[] { + const views: IViewDescriptor[] = []; + for (let i = 0; i < target.length; i++) { + if (this.allViews.has(target[i])) { + views.push(...Array.from(this.allViews.get(target[i])!.values())); + } + } + + return views; } - private getViewsNotForTarget(target: string): IViewDescriptor[] { + private getViewsNotForTarget(target: string[]): IViewDescriptor[] { const iterable = this.allViews.keys(); let key = iterable.next(); let views: IViewDescriptor[] = []; while (!key.done) { - if (key.value !== target) { - views = views.concat(this.getViewsForTarget(key.value)); + let isForTarget: boolean = false; + target.forEach(value => { + if (key.value === value) { + isForTarget = true; + } + }); + if (!isForTarget) { + views = views.concat(this.getViewsForTarget([key.value])); } + key = iterable.next(); } return views; diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 779bfce549..65e9d3fc6f 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -402,7 +402,7 @@ import { URI } from 'vs/base/common/uri'; @IExtensionService extensionService: IExtensionService, @IWorkspaceContextService contextService: IWorkspaceContextService ) { - super(viewContainer.id, (instantiationService as any).createInstance(viewContainer.ctorDescriptor!.ctor, ...(viewContainer.ctorDescriptor!.arguments || [])), telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService); + super(viewContainer.id, (instantiationService as any).createInstance(viewContainer.ctorDescriptor!.ctor, ...(viewContainer.ctorDescriptor!.staticArguments || [])), telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService); } } Registry.as(PanelExtensions.Panels).registerPanel(PanelDescriptor.create( @@ -429,7 +429,7 @@ import { URI } from 'vs/base/common/uri'; @IContextMenuService contextMenuService: IContextMenuService, @IExtensionService extensionService: IExtensionService ) { - super(viewContainer.id, (instantiationService as any).createInstance(viewContainer.ctorDescriptor!.ctor, ...(viewContainer.ctorDescriptor!.arguments || [])), telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService, layoutService, configurationService); + super(viewContainer.id, (instantiationService as any).createInstance(viewContainer.ctorDescriptor!.ctor, ...(viewContainer.ctorDescriptor!.staticArguments || [])), telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService, layoutService, configurationService); } } const viewletDescriptor = ViewletDescriptor.create( diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index c37abb750b..8af20f2506 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -5,11 +5,11 @@ import { Event, Emitter } from 'vs/base/common/event'; import { assign } from 'vs/base/common/objects'; -import { isUndefinedOrNull, withNullAsUndefined, assertIsDefined } from 'vs/base/common/types'; +import { withNullAsUndefined, assertIsDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IEditor as ICodeEditor, IEditorViewState, ScrollType, IDiffEditor } from 'vs/editor/common/editorCommon'; -import { IEditorModel, IEditorOptions, ITextEditorOptions, IBaseResourceInput, IResourceInput, EditorActivation, EditorOpenContext } from 'vs/platform/editor/common/editor'; +import { IEditorModel, IEditorOptions, ITextEditorOptions, IBaseResourceInput, IResourceInput, EditorActivation, EditorOpenContext, ITextEditorSelection } from 'vs/platform/editor/common/editor'; import { IInstantiationService, IConstructorSignature0, ServicesAccessor, BrandedService } from 'vs/platform/instantiation/common/instantiation'; import { RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -24,6 +24,7 @@ import { ITextFileSaveOptions, ITextFileService } from 'vs/workbench/services/te import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { isEqual } from 'vs/base/common/resources'; import { IPanel } from 'vs/workbench/common/panel'; +import { IRange } from 'vs/editor/common/core/range'; export const DirtyWorkingCopiesContext = new RawContextKey('dirtyWorkingCopies', false); export const ActiveEditorContext = new RawContextKey('activeEditor', null); @@ -220,7 +221,9 @@ export interface IUntitledTextResourceInput extends IBaseResourceInput { /** * Optional resource. If the resource is not provided a new untitled file is created (e.g. Untitled-1). - * Otherwise the untitled text editor will have an associated path and use that when saving. + * If the used scheme for the resource is not `untitled://`, `forceUntitled: true` must be configured to + * force use the provided resource as associated path. As such, the resource will be used when saving + * the untitled editor. */ resource?: URI; @@ -317,7 +320,7 @@ export interface ISaveOptions { context?: SaveContext; /** - * Forces to load the contents of the working copy + * Forces to save the contents of the working copy * again even if the working copy is not dirty. */ force?: boolean; @@ -403,6 +406,14 @@ export interface IEditorInput extends IDisposable { */ isDirty(): boolean; + /** + * Returns if this input is currently being saved or soon to be + * saved. Based on this assumption the editor may for example + * decide to not signal the dirty state to the user assuming that + * the save is scheduled to happen anyway. + */ + isSaving(): boolean; + /** * Saves the editor. The provided groupId helps * implementors to e.g. preserve view state of the editor @@ -508,16 +519,20 @@ export abstract class EditorInput extends Disposable implements IEditorInput { return false; } - save(groupId: GroupIdentifier, options?: ISaveOptions): Promise { - return Promise.resolve(true); + isSaving(): boolean { + return false; } - saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise { - return Promise.resolve(true); + async save(groupId: GroupIdentifier, options?: ISaveOptions): Promise { + return true; } - revert(options?: IRevertOptions): Promise { - return Promise.resolve(true); + async saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise { + return true; + } + + async revert(options?: IRevertOptions): Promise { + return true; } /** @@ -559,10 +574,6 @@ export abstract class TextEditorInput extends EditorInput { } async save(groupId: GroupIdentifier, options?: ITextFileSaveOptions): Promise { - if (this.isReadonly()) { - return false; // return early if editor is readonly - } - return this.textFileService.save(this.resource, options); } @@ -701,6 +712,10 @@ export class SideBySideEditorInput extends EditorInput { return this.master.isDirty(); } + isSaving(): boolean { + return this.master.isSaving(); + } + save(groupId: GroupIdentifier, options?: ISaveOptions): Promise { return this.master.save(groupId, options); } @@ -741,8 +756,8 @@ export class SideBySideEditorInput extends EditorInput { this._register(this.master.onDidChangeLabel(() => this._onDidChangeLabel.fire())); } - resolve(): Promise { - return Promise.resolve(null); + async resolve(): Promise { + return null; } getTypeId(): string { @@ -791,8 +806,8 @@ export class EditorModel extends Disposable implements IEditorModel { /** * Causes this model to load returning a promise when loading is completed. */ - load(): Promise { - return Promise.resolve(this); + async load(): Promise { + return this; } /** @@ -971,14 +986,22 @@ export class EditorOptions implements IEditorOptions { /** * Base Text Editor Options. */ -export class TextEditorOptions extends EditorOptions { - private startLineNumber: number | undefined; - private startColumn: number | undefined; - private endLineNumber: number | undefined; - private endColumn: number | undefined; +export class TextEditorOptions extends EditorOptions implements ITextEditorOptions { - private revealInCenterIfOutsideViewport: boolean | undefined; - private editorViewState: IEditorViewState | undefined; + /** + * Text editor selection. + */ + selection: ITextEditorSelection | undefined; + + /** + * Text editor view state. + */ + editorViewState: IEditorViewState | undefined; + + /** + * Option to scroll vertically or horizontally as necessary and reveal a range centered vertically only if it lies outside the viewport. + */ + revealInCenterIfOutsideViewport: boolean | undefined; static from(input?: IBaseResourceInput): TextEditorOptions | undefined { if (!input || !input.options) { @@ -1005,8 +1028,12 @@ export class TextEditorOptions extends EditorOptions { super.overwrite(options); if (options.selection) { - const selection = options.selection; - this.selection(selection.startLineNumber, selection.startColumn, selection.endLineNumber, selection.endColumn); + this.selection = { + startLineNumber: options.selection.startLineNumber, + startColumn: options.selection.startColumn, + endLineNumber: options.selection.endLineNumber ?? options.selection.startLineNumber, + endColumn: options.selection.endColumn ?? options.selection.startColumn + }; } if (options.viewState) { @@ -1024,19 +1051,7 @@ export class TextEditorOptions extends EditorOptions { * Returns if this options object has objects defined for the editor. */ hasOptionsDefined(): boolean { - return !!this.editorViewState || (!isUndefinedOrNull(this.startLineNumber) && !isUndefinedOrNull(this.startColumn)); - } - - /** - * Tells the editor to set show the given selection when the editor is being opened. - */ - selection(startLineNumber: number, startColumn: number, endLineNumber: number = startLineNumber, endColumn: number = startColumn): EditorOptions { - this.startLineNumber = startLineNumber; - this.startColumn = startColumn; - this.endLineNumber = endLineNumber; - this.endColumn = endColumn; - - return this; + return !!this.editorViewState || !!this.revealInCenterIfOutsideViewport || !!this.selection; } /** @@ -1057,12 +1072,6 @@ export class TextEditorOptions extends EditorOptions { * @return if something was applied */ apply(editor: ICodeEditor, scrollType: ScrollType): boolean { - - // View state - return this.applyViewState(editor, scrollType); - } - - private applyViewState(editor: ICodeEditor, scrollType: ScrollType): boolean { let gotApplied = false; // First try viewstate @@ -1072,36 +1081,20 @@ export class TextEditorOptions extends EditorOptions { } // Otherwise check for selection - else if (!isUndefinedOrNull(this.startLineNumber) && !isUndefinedOrNull(this.startColumn)) { + else if (this.selection) { + const range: IRange = { + startLineNumber: this.selection.startLineNumber, + startColumn: this.selection.startColumn, + endLineNumber: this.selection.endLineNumber ?? this.selection.startLineNumber, + endColumn: this.selection.endColumn ?? this.selection.startColumn + }; - // Select - if (!isUndefinedOrNull(this.endLineNumber) && !isUndefinedOrNull(this.endColumn)) { - const range = { - startLineNumber: this.startLineNumber, - startColumn: this.startColumn, - endLineNumber: this.endLineNumber, - endColumn: this.endColumn - }; - editor.setSelection(range); - if (this.revealInCenterIfOutsideViewport) { - editor.revealRangeInCenterIfOutsideViewport(range, scrollType); - } else { - editor.revealRangeInCenter(range, scrollType); - } - } + editor.setSelection(range); - // Reveal - else { - const pos = { - lineNumber: this.startLineNumber, - column: this.startColumn - }; - editor.setPosition(pos); - if (this.revealInCenterIfOutsideViewport) { - editor.revealPositionInCenterIfOutsideViewport(pos, scrollType); - } else { - editor.revealPositionInCenter(pos, scrollType); - } + if (this.revealInCenterIfOutsideViewport) { + editor.revealRangeInCenterIfOutsideViewport(range, scrollType); + } else { + editor.revealRangeInCenter(range, scrollType); } gotApplied = true; @@ -1307,13 +1300,13 @@ export async function pathsToEditors(paths: IPathData[] | undefined, fileService const exists = (typeof path.exists === 'boolean') ? path.exists : await fileService.exists(resource); - const options: ITextEditorOptions = { pinned: true }; - if (exists && typeof path.lineNumber === 'number') { - options.selection = { + const options: ITextEditorOptions = (exists && typeof path.lineNumber === 'number') ? { + selection: { startLineNumber: path.lineNumber, startColumn: path.columnNumber || 1 - }; - } + }, + pinned: true + } : { pinned: true }; let input: IResourceInput | IUntitledTextResourceInput; if (!exists) { diff --git a/src/vs/workbench/common/editor/diffEditorInput.ts b/src/vs/workbench/common/editor/diffEditorInput.ts index f3600a257d..f12d353cfd 100644 --- a/src/vs/workbench/common/editor/diffEditorInput.ts +++ b/src/vs/workbench/common/editor/diffEditorInput.ts @@ -28,6 +28,14 @@ export class DiffEditorInput extends SideBySideEditorInput { super(name, description, original, modified); } + matches(otherInput: unknown): boolean { + if (!super.matches(otherInput)) { + return false; + } + + return otherInput instanceof DiffEditorInput && otherInput.forceOpenAsBinary === this.forceOpenAsBinary; + } + getTypeId(): string { return DiffEditorInput.ID; } diff --git a/src/vs/workbench/common/editor/resourceEditorInput.ts b/src/vs/workbench/common/editor/resourceEditorInput.ts index 0f5f948680..eea5d5abfc 100644 --- a/src/vs/workbench/common/editor/resourceEditorInput.ts +++ b/src/vs/workbench/common/editor/resourceEditorInput.ts @@ -3,12 +3,15 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EditorInput, ITextEditorModel, IModeSupport } from 'vs/workbench/common/editor'; +import { EditorInput, ITextEditorModel, IModeSupport, GroupIdentifier, isTextEditor } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; import { IReference } from 'vs/base/common/lifecycle'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel'; import { basename } from 'vs/base/common/resources'; +import { ITextFileSaveOptions, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IEditorViewState } from 'vs/editor/common/editorCommon'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; /** * A read-only text editor input whos contents are made of the provided resource that points to an existing @@ -26,7 +29,9 @@ export class ResourceEditorInput extends EditorInput implements IModeSupport { private description: string | undefined, private readonly resource: URI, private preferredMode: string | undefined, - @ITextModelService private readonly textModelResolverService: ITextModelService + @ITextModelService private readonly textModelResolverService: ITextModelService, + @ITextFileService private readonly textFileService: ITextFileService, + @IEditorService private readonly editorService: IEditorService ) { super(); @@ -104,6 +109,28 @@ export class ResourceEditorInput extends EditorInput implements IModeSupport { return model; } + async saveAs(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise { + + // Preserve view state by opening the editor first. In addition + // this allows the user to review the contents of the editor. + let viewState: IEditorViewState | undefined = undefined; + const editor = await this.editorService.openEditor(this, undefined, group); + if (isTextEditor(editor)) { + viewState = editor.getViewState(); + } + + // Save as + const target = await this.textFileService.saveAs(this.resource, undefined, options); + if (!target) { + return false; // save cancelled + } + + // Open the target + await this.editorService.openEditor({ resource: target, options: { viewState, pinned: true } }, group); + + return true; + } + matches(otherInput: unknown): boolean { if (super.matches(otherInput) === true) { return true; diff --git a/src/vs/workbench/common/editor/untitledTextEditorInput.ts b/src/vs/workbench/common/editor/untitledTextEditorInput.ts index f24cff8336..fcba7e8d9e 100644 --- a/src/vs/workbench/common/editor/untitledTextEditorInput.ts +++ b/src/vs/workbench/common/editor/untitledTextEditorInput.ts @@ -28,19 +28,18 @@ export class UntitledTextEditorInput extends TextEditorInput implements IEncodin private static readonly MEMOIZER = createMemoizer(); + private readonly _onDidModelChangeEncoding = this._register(new Emitter()); + readonly onDidModelChangeEncoding = this._onDidModelChangeEncoding.event; + private cachedModel: UntitledTextEditorModel | null = null; private modelResolve: Promise | null = null; - private readonly _onDidModelChangeContent = this._register(new Emitter()); - readonly onDidModelChangeContent = this._onDidModelChangeContent.event; - - private readonly _onDidModelChangeEncoding = this._register(new Emitter()); - readonly onDidModelChangeEncoding = this._onDidModelChangeEncoding.event; + private preferredMode: string | undefined; constructor( resource: URI, private readonly _hasAssociatedFilePath: boolean, - private preferredMode: string | undefined, + preferredMode: string | undefined, private readonly initialValue: string | undefined, private preferredEncoding: string | undefined, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -52,6 +51,10 @@ export class UntitledTextEditorInput extends TextEditorInput implements IEncodin ) { super(resource, editorService, editorGroupService, textFileService); + if (preferredMode) { + this.setMode(preferredMode); + } + this.registerListeners(); } @@ -141,6 +144,8 @@ export class UntitledTextEditorInput extends TextEditorInput implements IEncodin } isDirty(): boolean { + + // Always trust the model first if existing if (this.cachedModel) { return this.cachedModel.isDirty(); } @@ -150,7 +155,13 @@ export class UntitledTextEditorInput extends TextEditorInput implements IEncodin return false; } - // untitled files with an associated path or associated resource + // A input with initial value is always dirty + if (this.initialValue && this.initialValue.length > 0) { + return true; + } + + // A input with associated path is always dirty because it is the intent + // of the user to create a new file at that location through saving return this.hasAssociatedFilePath; } @@ -225,10 +236,20 @@ export class UntitledTextEditorInput extends TextEditorInput implements IEncodin } setMode(mode: string): void { - this.preferredMode = mode; + let actualMode: string | undefined = undefined; + if (mode === '${activeEditorLanguage}') { + // support the special '${activeEditorLanguage}' mode by + // looking up the language mode from the currently + // active text editor if any + actualMode = this.editorService.activeTextEditorMode; + } else { + actualMode = mode; + } - if (this.cachedModel) { - this.cachedModel.setMode(mode); + this.preferredMode = actualMode; + + if (this.preferredMode && this.cachedModel) { + this.cachedModel.setMode(this.preferredMode); } } @@ -258,7 +279,6 @@ export class UntitledTextEditorInput extends TextEditorInput implements IEncodin const model = this._register(this.instantiationService.createInstance(UntitledTextEditorModel, this.preferredMode, this.resource, this.hasAssociatedFilePath, this.initialValue, this.preferredEncoding)); // re-emit some events from the model - this._register(model.onDidChangeContent(() => this._onDidModelChangeContent.fire())); this._register(model.onDidChangeDirty(() => this._onDidChangeDirty.fire())); this._register(model.onDidChangeEncoding(() => this._onDidModelChangeEncoding.fire())); diff --git a/src/vs/workbench/common/editor/untitledTextEditorModel.ts b/src/vs/workbench/common/editor/untitledTextEditorModel.ts index 7a3a2194a9..258e94321f 100644 --- a/src/vs/workbench/common/editor/untitledTextEditorModel.ts +++ b/src/vs/workbench/common/editor/untitledTextEditorModel.ts @@ -3,41 +3,38 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IEncodingSupport, ISaveOptions } from 'vs/workbench/common/editor'; +import { IEncodingSupport, ISaveOptions, IModeSupport } from 'vs/workbench/common/editor'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { URI } from 'vs/base/common/uri'; -import { CONTENT_CHANGE_EVENT_BUFFER_DELAY } from 'vs/platform/files/common/files'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { Event, Emitter } from 'vs/base/common/event'; -import { RunOnceScheduler } from 'vs/base/common/async'; +import { Emitter } from 'vs/base/common/event'; import { IBackupFileService, IResolvedBackup } from 'vs/workbench/services/backup/common/backup'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; import { ITextBufferFactory } from 'vs/editor/common/model'; import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; -import { IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; -import { IWorkingCopyService, IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IResolvedTextEditorModel, ITextEditorModel } from 'vs/editor/common/services/resolverService'; +import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -export class UntitledTextEditorModel extends BaseTextEditorModel implements IEncodingSupport, IWorkingCopy { +export interface IUntitledTextEditorModel extends ITextEditorModel, IModeSupport, IEncodingSupport, IWorkingCopy { } - static DEFAULT_CONTENT_CHANGE_BUFFER_DELAY = CONTENT_CHANGE_EVENT_BUFFER_DELAY; +export class UntitledTextEditorModel extends BaseTextEditorModel implements IUntitledTextEditorModel { - private readonly _onDidChangeContent: Emitter = this._register(new Emitter()); - readonly onDidChangeContent: Event = this._onDidChangeContent.event; + private readonly _onDidChangeContent = this._register(new Emitter()); + readonly onDidChangeContent = this._onDidChangeContent.event; - private readonly _onDidChangeDirty: Emitter = this._register(new Emitter()); - readonly onDidChangeDirty: Event = this._onDidChangeDirty.event; + private readonly _onDidChangeDirty = this._register(new Emitter()); + readonly onDidChangeDirty = this._onDidChangeDirty.event; - private readonly _onDidChangeEncoding: Emitter = this._register(new Emitter()); - readonly onDidChangeEncoding: Event = this._onDidChangeEncoding.event; + private readonly _onDidChangeEncoding = this._register(new Emitter()); + readonly onDidChangeEncoding = this._onDidChangeEncoding.event; - readonly capabilities = 0; + readonly capabilities = WorkingCopyCapabilities.Untitled; private dirty = false; private versionId = 0; - private readonly contentChangeEventScheduler = this._register(new RunOnceScheduler(() => this._onDidChangeContent.fire(), UntitledTextEditorModel.DEFAULT_CONTENT_CHANGE_BUFFER_DELAY)); - private configuredEncoding?: string; + private configuredEncoding: string | undefined; constructor( private readonly preferredMode: string | undefined, @@ -126,18 +123,13 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IEnc async revert(): Promise { this.setDirty(false); - // Handle content change event buffered - this.contentChangeEventScheduler.schedule(); - return true; } - backup(): Promise { + async backup(): Promise { if (this.isResolved()) { return this.backupFileService.backupResource(this.resource, this.createSnapshot(), this.versionId); } - - return Promise.resolve(); } hasBackup(): boolean { @@ -206,8 +198,8 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IEnc this.setDirty(true); } - // Handle content change event buffered - this.contentChangeEventScheduler.schedule(); + // Emit as event + this._onDidChangeContent.fire(); } isReadonly(): boolean { diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 5267999cd9..b02d7ed3ab 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -19,6 +19,7 @@ import { IAction } from 'vs/base/common/actions'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { flatten } from 'vs/base/common/arrays'; import { IViewPaneContainer } from 'vs/workbench/common/viewPaneContainer'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; export const TEST_VIEW_CONTAINER_ID = 'workbench.view.extension.test'; export const FocusedViewContext = new RawContextKey('focusedView', ''); @@ -39,7 +40,7 @@ export interface IViewContainerDescriptor { readonly name: string; - readonly ctorDescriptor: { ctor: new (...args: any[]) => IViewPaneContainer, arguments?: any[] }; + readonly ctorDescriptor: SyncDescriptor; readonly icon?: string | URI; @@ -99,6 +100,11 @@ export interface IViewContainersRegistry { * Returns all view containers in the given location */ getViewContainers(location: ViewContainerLocation): ViewContainer[]; + + /** + * Returns the view container location + */ + getViewContainerLocation(container: ViewContainer): ViewContainerLocation | undefined; } interface ViewOrderDelegate { @@ -156,6 +162,10 @@ class ViewContainersRegistryImpl extends Disposable implements IViewContainersRe getViewContainers(location: ViewContainerLocation): ViewContainer[] { return [...(this.viewContainers.get(location) || [])]; } + + getViewContainerLocation(container: ViewContainer): ViewContainerLocation | undefined { + return keys(this.viewContainers).filter(location => this.getViewContainers(location).filter(viewContainer => viewContainer.id === container.id).length > 0)[0]; + } } Registry.add(Extensions.ViewContainersRegistry, new ViewContainersRegistryImpl()); @@ -166,7 +176,7 @@ export interface IViewDescriptor { readonly name: string; - readonly ctorDescriptor: { ctor: any, arguments?: any[] }; + readonly ctorDescriptor: SyncDescriptor; readonly when?: ContextKeyExpr; @@ -192,6 +202,7 @@ export interface IViewDescriptor { } export interface IViewDescriptorCollection extends IDisposable { + readonly onDidChangeViews: Event<{ added: IViewDescriptor[], removed: IViewDescriptor[] }>; readonly onDidChangeActiveViews: Event<{ added: IViewDescriptor[], removed: IViewDescriptor[] }>; readonly activeViewDescriptors: IViewDescriptor[]; readonly allViewDescriptors: IViewDescriptor[]; @@ -335,14 +346,26 @@ export interface IViewsViewlet extends IViewlet { } +export const IViewDescriptorService = createDecorator('viewDescriptorService'); export const IViewsService = createDecorator('viewsService'); + export interface IViewsService { _serviceBrand: undefined; openView(id: string, focus?: boolean): Promise; +} - getViewDescriptors(container: ViewContainer): IViewDescriptorCollection | null; + +export interface IViewDescriptorService { + + _serviceBrand: undefined; + + moveViews(views: IViewDescriptor[], viewContainer: ViewContainer): void; + + getViewDescriptors(container: ViewContainer): IViewDescriptorCollection; + + getViewContainer(viewId: string): ViewContainer | null; } // Custom views diff --git a/src/vs/workbench/contrib/backup/browser/backup.web.contribution.ts b/src/vs/workbench/contrib/backup/browser/backup.web.contribution.ts new file mode 100644 index 0000000000..1457c821be --- /dev/null +++ b/src/vs/workbench/contrib/backup/browser/backup.web.contribution.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { BackupOnShutdown } from 'vs/workbench/contrib/backup/browser/backupOnShutdown'; + +// Register Backup On Shutdown +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BackupOnShutdown, LifecyclePhase.Starting); diff --git a/src/vs/workbench/contrib/backup/browser/backupOnShutdown.ts b/src/vs/workbench/contrib/backup/browser/backupOnShutdown.ts new file mode 100644 index 0000000000..a8f6e09849 --- /dev/null +++ b/src/vs/workbench/contrib/backup/browser/backupOnShutdown.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; + +export class BackupOnShutdown extends Disposable implements IWorkbenchContribution { + + constructor( + @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, + @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, + @ILifecycleService private readonly lifecycleService: ILifecycleService, + ) { + super(); + + this.registerListeners(); + } + + private registerListeners() { + + // Lifecycle + this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdown())); + } + + private onBeforeShutdown(): boolean { + + // Web: we cannot perform long running in the shutdown phase + // As such we need to check sync if there are any dirty working + // copies that have not been backed up yet and then prevent the + // shutdown if that is the case. + + const dirtyWorkingCopies = this.workingCopyService.dirtyWorkingCopies; + if (!dirtyWorkingCopies.length) { + return false; // no dirty: no veto + } + + if (!this.filesConfigurationService.isHotExitEnabled) { + return true; // dirty without backup: veto + } + + for (const dirtyWorkingCopy of dirtyWorkingCopies) { + if (!dirtyWorkingCopy.hasBackup()) { + console.warn('Unload prevented: pending backups'); + return true; // dirty without backup: veto + } + } + + return false; // dirty with backups: no veto + } +} diff --git a/src/vs/workbench/contrib/backup/common/backup.contribution.ts b/src/vs/workbench/contrib/backup/common/backup.contribution.ts index 6dbf82cfd3..8dcfba0ef7 100644 --- a/src/vs/workbench/contrib/backup/common/backup.contribution.ts +++ b/src/vs/workbench/contrib/backup/common/backup.contribution.ts @@ -5,12 +5,12 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { BackupModelTracker } from 'vs/workbench/contrib/backup/common/backupModelTracker'; +import { BackupTracker } from 'vs/workbench/contrib/backup/common/backupTracker'; import { BackupRestorer } from 'vs/workbench/contrib/backup/common/backupRestorer'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -// Register Backup Model Tracker -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BackupModelTracker, LifecyclePhase.Starting); +// Register Backup Tracker +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BackupTracker, LifecyclePhase.Starting); // Register Backup Restorer -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BackupRestorer, LifecyclePhase.Starting); \ No newline at end of file +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BackupRestorer, LifecyclePhase.Starting); diff --git a/src/vs/workbench/contrib/backup/common/backupModelTracker.ts b/src/vs/workbench/contrib/backup/common/backupModelTracker.ts deleted file mode 100644 index 5a966aaa10..0000000000 --- a/src/vs/workbench/contrib/backup/common/backupModelTracker.ts +++ /dev/null @@ -1,85 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { URI as Uri } from 'vs/base/common/uri'; -import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { ITextFileService, TextFileModelChangeEvent, StateChange } from 'vs/workbench/services/textfile/common/textfiles'; -import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; -import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { CONTENT_CHANGE_EVENT_BUFFER_DELAY } from 'vs/platform/files/common/files'; -import { IFilesConfigurationService, IAutoSaveConfiguration } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; - -const AUTO_SAVE_AFTER_DELAY_DISABLED_TIME = CONTENT_CHANGE_EVENT_BUFFER_DELAY + 500; - -export class BackupModelTracker extends Disposable implements IWorkbenchContribution { - - private configuredAutoSaveAfterDelay = false; - - constructor( - @IBackupFileService private readonly backupFileService: IBackupFileService, - @ITextFileService private readonly textFileService: ITextFileService, - @IUntitledTextEditorService private readonly untitledTextEditorService: IUntitledTextEditorService, - @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService - ) { - super(); - - this.registerListeners(); - } - - private registerListeners() { - - // Listen for text file model changes - this._register(this.textFileService.models.onModelContentChanged(e => this.onTextFileModelChanged(e))); - this._register(this.textFileService.models.onModelSaved(e => this.discardBackup(e.resource))); - this._register(this.textFileService.models.onModelDisposed(e => this.discardBackup(e))); - - // Listen for untitled model changes - this._register(this.untitledTextEditorService.onDidCreate(e => this.onUntitledModelCreated(e))); - this._register(this.untitledTextEditorService.onDidChangeContent(e => this.onUntitledModelChanged(e))); - this._register(this.untitledTextEditorService.onDidDisposeModel(e => this.discardBackup(e))); - - // Listen to auto save config changes - this._register(this.filesConfigurationService.onAutoSaveConfigurationChange(c => this.onAutoSaveConfigurationChange(c))); - } - - private onAutoSaveConfigurationChange(configuration: IAutoSaveConfiguration): void { - this.configuredAutoSaveAfterDelay = typeof configuration.autoSaveDelay === 'number' && configuration.autoSaveDelay < AUTO_SAVE_AFTER_DELAY_DISABLED_TIME; - } - - private onTextFileModelChanged(event: TextFileModelChangeEvent): void { - if (event.kind === StateChange.REVERTED) { - // This must proceed even if auto save after delay is configured in order to clean up - // any backups made before the config change - this.discardBackup(event.resource); - } else if (event.kind === StateChange.CONTENT_CHANGE) { - // Do not backup when auto save after delay is configured - if (!this.configuredAutoSaveAfterDelay) { - const model = this.textFileService.models.get(event.resource); - if (model) { - model.backup(); - } - } - } - } - - private onUntitledModelCreated(resource: Uri): void { - if (this.untitledTextEditorService.isDirty(resource)) { - this.untitledTextEditorService.loadOrCreate({ resource }).then(model => model.backup()); - } - } - - private onUntitledModelChanged(resource: Uri): void { - if (this.untitledTextEditorService.isDirty(resource)) { - this.untitledTextEditorService.loadOrCreate({ resource }).then(model => model.backup()); - } else { - this.discardBackup(resource); - } - } - - private discardBackup(resource: Uri): void { - this.backupFileService.discardResourceBackup(resource); - } -} diff --git a/src/vs/workbench/contrib/backup/common/backupTracker.ts b/src/vs/workbench/contrib/backup/common/backupTracker.ts new file mode 100644 index 0000000000..9d9cd21bde --- /dev/null +++ b/src/vs/workbench/contrib/backup/common/backupTracker.ts @@ -0,0 +1,124 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { Disposable, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IFilesConfigurationService, IAutoSaveConfiguration } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { ILogService } from 'vs/platform/log/common/log'; + +export class BackupTracker extends Disposable implements IWorkbenchContribution { + + // Disable backup for when a short auto-save delay is configured with + // the rationale that the auto save will trigger a save periodically + // anway and thus creating frequent backups is not useful + // + // This will only apply to working copies that are not untitled where + // auto save is actually saving. + private static DISABLE_BACKUP_AUTO_SAVE_THRESHOLD = 1500; + + // Delay creation of backups when content changes to avoid too much + // load on the backup service when the user is typing into the editor + protected static BACKUP_FROM_CONTENT_CHANGE_DELAY = 1000; + + private backupsDisabledForAutoSaveables = false; + + private readonly pendingBackups = new Map(); + + constructor( + @IBackupFileService private readonly backupFileService: IBackupFileService, + @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, + @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, + @ILogService private readonly logService: ILogService + ) { + super(); + + // Figure out initial auto save config + this.onAutoSaveConfigurationChange(filesConfigurationService.getAutoSaveConfiguration()); + + this.registerListeners(); + } + + private registerListeners() { + + // Working Copy events + this._register(this.workingCopyService.onDidRegister(c => this.onDidRegister(c))); + this._register(this.workingCopyService.onDidUnregister(c => this.onDidUnregister(c))); + this._register(this.workingCopyService.onDidChangeDirty(c => this.onDidChangeDirty(c))); + this._register(this.workingCopyService.onDidChangeContent(c => this.onDidChangeContent(c))); + + // Listen to auto save config changes + this._register(this.filesConfigurationService.onAutoSaveConfigurationChange(c => this.onAutoSaveConfigurationChange(c))); + } + + private onDidRegister(workingCopy: IWorkingCopy): void { + this.scheduleBackup(workingCopy); + } + + private onDidUnregister(workingCopy: IWorkingCopy): void { + this.discardBackup(workingCopy); + } + + private onDidChangeDirty(workingCopy: IWorkingCopy): void { + if (!workingCopy.isDirty()) { + this.discardBackup(workingCopy); + } + } + + private onDidChangeContent(workingCopy: IWorkingCopy): void { + if (workingCopy.isDirty()) { + this.scheduleBackup(workingCopy); + } + } + + private onAutoSaveConfigurationChange(configuration: IAutoSaveConfiguration): void { + this.backupsDisabledForAutoSaveables = typeof configuration.autoSaveDelay === 'number' && configuration.autoSaveDelay < BackupTracker.DISABLE_BACKUP_AUTO_SAVE_THRESHOLD; + } + + private scheduleBackup(workingCopy: IWorkingCopy): void { + if (this.backupsDisabledForAutoSaveables && !(workingCopy.capabilities & WorkingCopyCapabilities.Untitled)) { + return; // skip if auto save is enabled with a short delay + } + + // Clear any running backup operation + dispose(this.pendingBackups.get(workingCopy)); + this.pendingBackups.delete(workingCopy); + + this.logService.trace(`[backup tracker] scheduling backup`, workingCopy.resource.toString()); + + // Schedule new backup + const handle = setTimeout(() => { + + // Clear disposable + this.pendingBackups.delete(workingCopy); + + // Backup if dirty + if (workingCopy.isDirty()) { + this.logService.trace(`[backup tracker] running backup`, workingCopy.resource.toString()); + + workingCopy.backup(); + } + }, BackupTracker.BACKUP_FROM_CONTENT_CHANGE_DELAY); + + // Keep in map for disposal as needed + this.pendingBackups.set(workingCopy, toDisposable(() => { + this.logService.trace(`[backup tracker] clearing pending backup`, workingCopy.resource.toString()); + + clearTimeout(handle); + })); + } + + private discardBackup(workingCopy: IWorkingCopy): void { + this.logService.trace(`[backup tracker] discarding backup`, workingCopy.resource.toString()); + + // Clear any running backup operation + dispose(this.pendingBackups.get(workingCopy)); + this.pendingBackups.delete(workingCopy); + + // Forward to backup file service + this.backupFileService.discardResourceBackup(workingCopy.resource); + } +} diff --git a/src/vs/workbench/contrib/backup/electron-browser/backup.contribution.ts b/src/vs/workbench/contrib/backup/electron-browser/backup.contribution.ts new file mode 100644 index 0000000000..88638d05b4 --- /dev/null +++ b/src/vs/workbench/contrib/backup/electron-browser/backup.contribution.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { BackupOnShutdown } from 'vs/workbench/contrib/backup/electron-browser/backupOnShutdown'; + +// Register Backup On Shutdown +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BackupOnShutdown, LifecyclePhase.Starting); diff --git a/src/vs/workbench/contrib/backup/electron-browser/backupOnShutdown.ts b/src/vs/workbench/contrib/backup/electron-browser/backupOnShutdown.ts new file mode 100644 index 0000000000..a35f2a046c --- /dev/null +++ b/src/vs/workbench/contrib/backup/electron-browser/backupOnShutdown.ts @@ -0,0 +1,207 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { ILifecycleService, LifecyclePhase, ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { ConfirmResult, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { WorkbenchState, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { isMacintosh } from 'vs/base/common/platform'; +import { HotExitConfiguration } from 'vs/platform/files/common/files'; +import { IElectronService } from 'vs/platform/electron/node/electron'; +import type { ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; + +export class BackupOnShutdown extends Disposable implements IWorkbenchContribution { + + constructor( + @IBackupFileService private readonly backupFileService: IBackupFileService, + @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, + @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, + @ILifecycleService private readonly lifecycleService: ILifecycleService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IFileDialogService private readonly fileDialogService: IFileDialogService, + @INotificationService private readonly notificationService: INotificationService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, + @IElectronService private readonly electronService: IElectronService + ) { + super(); + + this.registerListeners(); + } + + private registerListeners() { + + // Lifecycle + this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdown(event.reason))); + } + + private onBeforeShutdown(reason: ShutdownReason): boolean | Promise { + + // Dirty working copies need treatment on shutdown + const dirtyWorkingCopies = this.workingCopyService.dirtyWorkingCopies; + if (dirtyWorkingCopies.length) { + + // If auto save is enabled, save all working copies and then check again for dirty copies + // We DO NOT run any save participant if we are in the shutdown phase for performance reasons + if (this.filesConfigurationService.getAutoSaveMode() !== AutoSaveMode.OFF) { + return this.doSaveAll(dirtyWorkingCopies, false /* not untitled */, { skipSaveParticipants: true }).then(() => { + + // If we still have dirty working copies, we either have untitled ones or working copies that cannot be saved + const remainingDirtyWorkingCopies = this.workingCopyService.dirtyWorkingCopies; + if (remainingDirtyWorkingCopies.length) { + return this.handleDirtyBeforeShutdown(remainingDirtyWorkingCopies, reason); + } + + return this.noVeto({ cleanUpBackups: false }); // no veto and no backup cleanup (since there are no dirty working copies) + }); + } + + // Auto save is not enabled + return this.handleDirtyBeforeShutdown(dirtyWorkingCopies, reason); + } + + // No dirty working copies: no veto + return this.noVeto({ cleanUpBackups: true }); + } + + private handleDirtyBeforeShutdown(workingCopies: IWorkingCopy[], reason: ShutdownReason): boolean | Promise { + + // If hot exit is enabled, backup dirty working copies and allow to exit without confirmation + if (this.filesConfigurationService.isHotExitEnabled) { + return this.backupBeforeShutdown(workingCopies, reason).then(didBackup => { + if (didBackup) { + return this.noVeto({ cleanUpBackups: false }); // no veto and no backup cleanup (since backup was successful) + } + + // since a backup did not happen, we have to confirm for the dirty working copies now + return this.confirmBeforeShutdown(); + }, error => { + this.notificationService.error(localize('backupOnShutdown.failSave', "Working copies that are dirty could not be written to the backup location (Error: {0}). Try saving your editors first and then exit.", error.message)); + + return true; // veto, the backups failed + }); + } + + // Otherwise just confirm from the user what to do with the dirty working copies + return this.confirmBeforeShutdown(); + } + + private async backupBeforeShutdown(workingCopies: IWorkingCopy[], reason: ShutdownReason): Promise { + + // When quit is requested skip the confirm callback and attempt to backup all workspaces. + // When quit is not requested the confirm callback should be shown when the window being + // closed is the only VS Code window open, except for on Mac where hot exit is only + // ever activated when quit is requested. + + let doBackup: boolean | undefined; + switch (reason) { + case ShutdownReason.CLOSE: + if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.filesConfigurationService.hotExitConfiguration === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) { + doBackup = true; // backup if a folder is open and onExitAndWindowClose is configured + } else if (await this.electronService.getWindowCount() > 1 || isMacintosh) { + doBackup = false; // do not backup if a window is closed that does not cause quitting of the application + } else { + doBackup = true; // backup if last window is closed on win/linux where the application quits right after + } + break; + + case ShutdownReason.QUIT: + doBackup = true; // backup because next start we restore all backups + break; + + case ShutdownReason.RELOAD: + doBackup = true; // backup because after window reload, backups restore + break; + + case ShutdownReason.LOAD: + if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.filesConfigurationService.hotExitConfiguration === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) { + doBackup = true; // backup if a folder is open and onExitAndWindowClose is configured + } else { + doBackup = false; // do not backup because we are switching contexts + } + break; + } + + if (!doBackup) { + return false; + } + + // Backup all working copies + await Promise.all(workingCopies.map(workingCopy => workingCopy.backup())); + + return true; + } + + private async confirmBeforeShutdown(): Promise { + + // Show confirm dialog for all dirty working copies + const dirtyWorkingCopies = this.workingCopyService.dirtyWorkingCopies; + const confirm = await this.fileDialogService.showSaveConfirm(dirtyWorkingCopies.map(w => w.resource)); + + // Save + if (confirm === ConfirmResult.SAVE) { + await this.doSaveAll(dirtyWorkingCopies, true /* includeUntitled */, { skipSaveParticipants: true }); + + if (this.workingCopyService.hasDirty) { + return true; // veto if any save failed + } + + return this.noVeto({ cleanUpBackups: true }); + } + + // Don't Save + else if (confirm === ConfirmResult.DONT_SAVE) { + + // Make sure to revert working copies so that they do not restore + // see https://github.com/Microsoft/vscode/issues/29572 + await this.doRevertAll(dirtyWorkingCopies, { soft: true } /* soft revert is good enough on shutdown */); + + return this.noVeto({ cleanUpBackups: true }); + } + + // Cancel + else if (confirm === ConfirmResult.CANCEL) { + return true; // veto + } + + return false; + } + + private doSaveAll(workingCopies: IWorkingCopy[], includeUntitled: boolean, options: ISaveOptions): Promise { + return Promise.all(workingCopies.map(async workingCopy => { + if (workingCopy.isDirty() && (includeUntitled || !(workingCopy.capabilities & WorkingCopyCapabilities.Untitled))) { + return workingCopy.save(options); + } + + return false; + })); + } + + private doRevertAll(workingCopies: IWorkingCopy[], options: IRevertOptions): Promise { + return Promise.all(workingCopies.map(workingCopy => workingCopy.revert(options))); + } + + private noVeto(options: { cleanUpBackups: boolean }): boolean | Promise { + if (!options.cleanUpBackups) { + return false; + } + + if (this.lifecycleService.phase < LifecyclePhase.Restored) { + return false; // if editors have not restored, we are not up to speed with backups and thus should not clean them + } + + if (this.environmentService.isExtensionDevelopment) { + return false; // extension development does not track any backups + } + + return this.backupFileService.discardAllWorkspaceBackups().then(() => false, () => false); + } +} diff --git a/src/vs/workbench/contrib/backup/test/electron-browser/backupOnShutdown.test.ts b/src/vs/workbench/contrib/backup/test/electron-browser/backupOnShutdown.test.ts new file mode 100644 index 0000000000..08b2536828 --- /dev/null +++ b/src/vs/workbench/contrib/backup/test/electron-browser/backupOnShutdown.test.ts @@ -0,0 +1,282 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as platform from 'vs/base/common/platform'; +import { ILifecycleService, BeforeShutdownEvent, ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle'; +import { workbenchInstantiationService, TestLifecycleService, TestTextFileService, TestContextService, TestFileService, TestElectronService, TestFilesConfigurationService, TestFileDialogService, TestBackupFileService } from 'vs/workbench/test/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 { HotExitConfiguration, IFileService } from 'vs/platform/files/common/files'; +import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; +import { IWorkspaceContextService, Workspace } from 'vs/platform/workspace/common/workspace'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; +import { IElectronService } from 'vs/platform/electron/node/electron'; +import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { IFileDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; +import { BackupOnShutdown } from 'vs/workbench/contrib/backup/electron-browser/backupOnShutdown'; +import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; + +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: TestBackupFileService, + @IWorkingCopyService public workingCopyService: IWorkingCopyService + ) { + } +} + +class BeforeShutdownEventImpl implements BeforeShutdownEvent { + + value: boolean | Promise | undefined; + reason = ShutdownReason.CLOSE; + + veto(value: boolean | Promise): void { + this.value = value; + } +} + +suite('BackupOnShutdown', () => { + + let instantiationService: IInstantiationService; + let model: TextFileEditorModel; + let accessor: ServiceAccessor; + let backupOnShutdown: BackupOnShutdown; + + setup(() => { + instantiationService = workbenchInstantiationService(); + accessor = instantiationService.createInstance(ServiceAccessor); + backupOnShutdown = instantiationService.createInstance(BackupOnShutdown); + }); + + teardown(() => { + if (model) { + model.dispose(); + } + (accessor.textFileService.files).dispose(); + backupOnShutdown.dispose(); + }); + + test('confirm onWillShutdown - no veto', async function () { + model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); + (accessor.textFileService.files).add(model.resource, model); + + const event = new BeforeShutdownEventImpl(); + accessor.lifecycleService.fireWillShutdown(event); + + const veto = event.value; + if (typeof veto === 'boolean') { + assert.ok(!veto); + } else { + assert.ok(!(await veto)); + } + }); + + test('confirm onWillShutdown - veto if user cancels', async function () { + model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); + (accessor.textFileService.files).add(model.resource, model); + + accessor.fileDialogService.setConfirmResult(ConfirmResult.CANCEL); + + await model.load(); + model.textEditorModel!.setValue('foo'); + assert.equal(accessor.workingCopyService.dirtyCount, 1); + + const event = new BeforeShutdownEventImpl(); + accessor.lifecycleService.fireWillShutdown(event); + assert.ok(event.value); + }); + + test('confirm onWillShutdown - no veto and backups cleaned up if user does not want to save (hot.exit: off)', async function () { + model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); + (accessor.textFileService.files).add(model.resource, model); + + accessor.fileDialogService.setConfirmResult(ConfirmResult.DONT_SAVE); + accessor.filesConfigurationService.onFilesConfigurationChange({ files: { hotExit: 'off' } }); + + await model.load(); + model.textEditorModel!.setValue('foo'); + assert.equal(accessor.workingCopyService.dirtyCount, 1); + const event = new BeforeShutdownEventImpl(); + accessor.lifecycleService.fireWillShutdown(event); + + let veto = event.value; + if (typeof veto === 'boolean') { + assert.ok(accessor.backupFileService.didDiscardAllWorkspaceBackups); + assert.ok(!veto); + return; + } + + veto = await veto; + assert.ok(accessor.backupFileService.didDiscardAllWorkspaceBackups); + assert.ok(!veto); + }); + + test('confirm onWillShutdown - save (hot.exit: off)', async function () { + model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); + (accessor.textFileService.files).add(model.resource, model); + + accessor.fileDialogService.setConfirmResult(ConfirmResult.SAVE); + accessor.filesConfigurationService.onFilesConfigurationChange({ files: { hotExit: 'off' } }); + + await model.load(); + model.textEditorModel!.setValue('foo'); + assert.equal(accessor.workingCopyService.dirtyCount, 1); + const event = new BeforeShutdownEventImpl(); + accessor.lifecycleService.fireWillShutdown(event); + + const veto = await (>event.value); + assert.ok(!veto); + assert.ok(!model.isDirty()); + }); + + suite('Hot Exit', () => { + suite('"onExit" setting', () => { + test('should hot exit on non-Mac (reason: CLOSE, windows: single, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.CLOSE, false, true, !!platform.isMacintosh); + }); + test('should hot exit on non-Mac (reason: CLOSE, windows: single, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.CLOSE, false, false, !!platform.isMacintosh); + }); + test('should NOT hot exit (reason: CLOSE, windows: multiple, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.CLOSE, true, true, true); + }); + test('should NOT hot exit (reason: CLOSE, windows: multiple, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.CLOSE, true, false, true); + }); + test('should hot exit (reason: QUIT, windows: single, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.QUIT, false, true, false); + }); + test('should hot exit (reason: QUIT, windows: single, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.QUIT, false, false, false); + }); + test('should hot exit (reason: QUIT, windows: multiple, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.QUIT, true, true, false); + }); + test('should hot exit (reason: QUIT, windows: multiple, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.QUIT, true, false, false); + }); + test('should hot exit (reason: RELOAD, windows: single, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.RELOAD, false, true, false); + }); + test('should hot exit (reason: RELOAD, windows: single, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.RELOAD, false, false, false); + }); + test('should hot exit (reason: RELOAD, windows: multiple, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.RELOAD, true, true, false); + }); + test('should hot exit (reason: RELOAD, windows: multiple, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.RELOAD, true, false, false); + }); + test('should NOT hot exit (reason: LOAD, windows: single, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.LOAD, false, true, true); + }); + test('should NOT hot exit (reason: LOAD, windows: single, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.LOAD, false, false, true); + }); + test('should NOT hot exit (reason: LOAD, windows: multiple, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.LOAD, true, true, true); + }); + test('should NOT hot exit (reason: LOAD, windows: multiple, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.LOAD, true, false, true); + }); + }); + + suite('"onExitAndWindowClose" setting', () => { + test('should hot exit (reason: CLOSE, windows: single, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.CLOSE, false, true, false); + }); + test('should hot exit (reason: CLOSE, windows: single, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.CLOSE, false, false, !!platform.isMacintosh); + }); + test('should hot exit (reason: CLOSE, windows: multiple, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.CLOSE, true, true, false); + }); + test('should NOT hot exit (reason: CLOSE, windows: multiple, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.CLOSE, true, false, true); + }); + test('should hot exit (reason: QUIT, windows: single, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.QUIT, false, true, false); + }); + test('should hot exit (reason: QUIT, windows: single, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.QUIT, false, false, false); + }); + test('should hot exit (reason: QUIT, windows: multiple, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.QUIT, true, true, false); + }); + test('should hot exit (reason: QUIT, windows: multiple, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.QUIT, true, false, false); + }); + test('should hot exit (reason: RELOAD, windows: single, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.RELOAD, false, true, false); + }); + test('should hot exit (reason: RELOAD, windows: single, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.RELOAD, false, false, false); + }); + test('should hot exit (reason: RELOAD, windows: multiple, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.RELOAD, true, true, false); + }); + test('should hot exit (reason: RELOAD, windows: multiple, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.RELOAD, true, false, false); + }); + test('should hot exit (reason: LOAD, windows: single, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.LOAD, false, true, false); + }); + test('should NOT hot exit (reason: LOAD, windows: single, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.LOAD, false, false, true); + }); + test('should hot exit (reason: LOAD, windows: multiple, workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.LOAD, true, true, false); + }); + test('should NOT hot exit (reason: LOAD, windows: multiple, empty workspace)', function () { + return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.LOAD, true, false, true); + }); + }); + + async function hotExitTest(this: any, setting: string, shutdownReason: ShutdownReason, multipleWindows: boolean, workspace: boolean, shouldVeto: boolean): Promise { + model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); + (accessor.textFileService.files).add(model.resource, model); + + // Set hot exit config + accessor.filesConfigurationService.onFilesConfigurationChange({ files: { hotExit: setting } }); + + // Set empty workspace if required + if (!workspace) { + accessor.contextService.setWorkspace(new Workspace('empty:1508317022751')); + } + + // Set multiple windows if required + if (multipleWindows) { + accessor.electronService.windowCount = Promise.resolve(2); + } + + // Set cancel to force a veto if hot exit does not trigger + accessor.fileDialogService.setConfirmResult(ConfirmResult.CANCEL); + + await model.load(); + model.textEditorModel!.setValue('foo'); + assert.equal(accessor.workingCopyService.dirtyCount, 1); + + const event = new BeforeShutdownEventImpl(); + event.reason = shutdownReason; + accessor.lifecycleService.fireWillShutdown(event); + + const veto = await (>event.value); + assert.ok(!accessor.backupFileService.didDiscardAllWorkspaceBackups); // When hot exit is set, backups should never be cleaned since the confirm result is cancel + assert.equal(veto, shouldVeto); + } + }); +}); 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 bda7e12073..0f50f0ae01 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 @@ -14,9 +14,8 @@ 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 { BackupModelTracker } from 'vs/workbench/contrib/backup/common/backupModelTracker'; +import { BackupTracker } from 'vs/workbench/contrib/backup/common/backupTracker'; import { TestTextFileService, workbenchInstantiationService } from 'vs/workbench/test/workbenchTestServices'; -import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; 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'; @@ -54,13 +53,12 @@ class TestBackupRestorer extends BackupRestorer { class ServiceAccessor { constructor( - @ITextFileService public textFileService: TestTextFileService, - @IUntitledTextEditorService public untitledTextEditorService: IUntitledTextEditorService + @ITextFileService public textFileService: TestTextFileService ) { } } -suite.skip('BackupModelRestorer', () => { // {{SQL CARBON EDIT}} TODO @anthonydresser these tests are failing due to tabColorMode, should investigate and fix +suite.skip('BackupRestorer', () => { // {{SQL CARBON EDIT}} TODO @anthonydresser these tests are failing due to tabColorMode, should investigate and fix let accessor: ServiceAccessor; let disposables: IDisposable[] = []; @@ -86,9 +84,7 @@ suite.skip('BackupModelRestorer', () => { // {{SQL CARBON EDIT}} TODO @anthonydr dispose(disposables); disposables = []; - (accessor.textFileService.models).clear(); - (accessor.textFileService.models).dispose(); - accessor.untitledTextEditorService.revertAll(); + (accessor.textFileService.files).dispose(); return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE); }); @@ -111,7 +107,9 @@ suite.skip('BackupModelRestorer', () => { // {{SQL CARBON EDIT}} TODO @anthonydr accessor = instantiationService.createInstance(ServiceAccessor); - const tracker = instantiationService.createInstance(BackupModelTracker); + await part.whenRestored; + + const tracker = instantiationService.createInstance(BackupTracker); const restorer = instantiationService.createInstance(TestBackupRestorer); // Backup 2 normal files and 2 untitled file @@ -129,19 +127,21 @@ suite.skip('BackupModelRestorer', () => { // {{SQL CARBON EDIT}} TODO @anthonydr for (const editor of editorService.editors) { const resource = editor.getResource(); if (isEqual(resource, untitledFile1)) { - const model = await accessor.untitledTextEditorService.createOrGet(resource).resolve(); + const model = await accessor.textFileService.untitled.resolve({ untitledResource: resource }); assert.equal(model.textEditorModel.getValue(), 'untitled-1'); + model.dispose(); counter++; } else if (isEqual(resource, untitledFile2)) { - const model = await accessor.untitledTextEditorService.createOrGet(resource).resolve(); + const model = await accessor.textFileService.untitled.resolve({ untitledResource: resource }); assert.equal(model.textEditorModel.getValue(), 'untitled-2'); + model.dispose(); counter++; } else if (isEqual(resource, fooFile)) { - const model = await accessor.textFileService.models.get(resource!)?.load(); + const model = await accessor.textFileService.files.get(resource!)?.load(); assert.equal(model?.textEditorModel?.getValue(), 'fooFile'); counter++; } else { - const model = await accessor.textFileService.models.get(resource!)?.load(); + const model = await accessor.textFileService.files.get(resource!)?.load(); assert.equal(model?.textEditorModel?.getValue(), 'barFile'); counter++; } 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 new file mode 100644 index 0000000000..99c71ec862 --- /dev/null +++ b/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts @@ -0,0 +1,170 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as platform from 'vs/base/common/platform'; +import * as os from 'os'; +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 { BackupTracker } from 'vs/workbench/contrib/backup/common/backupTracker'; +import { TestTextFileService, workbenchInstantiationService } from 'vs/workbench/test/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'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { EditorInput } from 'vs/workbench/common/editor'; +import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; +import { TextFileEditor } from 'vs/workbench/contrib/files/browser/editors/textFileEditor'; +import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { NodeTestBackupFileService } from 'vs/workbench/services/backup/test/electron-browser/backupFileService.test'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +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'; + +const userdataDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backuprestorer'); +const backupHome = path.join(userdataDir, 'Backups'); +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( + @ITextFileService public textFileService: TestTextFileService, + @IEditorService public editorService: IEditorService, + @IBackupFileService public backupFileService: NodeTestBackupFileService + ) { + } +} + +class TestBackupTracker extends BackupTracker { + + constructor( + @IBackupFileService backupFileService: IBackupFileService, + @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, + @IWorkingCopyService workingCopyService: IWorkingCopyService, + @ILogService logService: ILogService + ) { + super(backupFileService, filesConfigurationService, workingCopyService, logService); + + // Reduce timeout for tests + BackupTracker.BACKUP_FROM_CONTENT_CHANGE_DELAY = 10; + } +} + +suite('BackupTracker', () => { + let accessor: ServiceAccessor; + + let disposables: IDisposable[] = []; + + setup(async () => { + disposables.push(Registry.as(EditorExtensions.Editors).registerEditor( + EditorDescriptor.create( + TextFileEditor, + TextFileEditor.ID, + 'Text File Editor' + ), + [new SyncDescriptor(FileEditorInput)] + )); + + // Delete any existing backups completely and then re-create it. + await pfs.rimraf(backupHome, pfs.RimRafMode.MOVE); + await pfs.mkdirp(backupHome); + + return pfs.writeFile(workspacesJsonPath, ''); + }); + + teardown(async () => { + dispose(disposables); + disposables = []; + + (accessor.textFileService.files).dispose(); + + return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE); + }); + + async function createTracker(): Promise<[ServiceAccessor, EditorPart, BackupTracker]> { + const backupFileService = new NodeTestBackupFileService(workspaceBackupPath); + const instantiationService = workbenchInstantiationService(); + instantiationService.stub(IBackupFileService, backupFileService); + + const part = instantiationService.createInstance(EditorPart); + part.create(document.createElement('div')); + part.layout(400, 300); + + instantiationService.stub(IEditorGroupsService, part); + + const editorService: EditorService = instantiationService.createInstance(EditorService); + instantiationService.stub(IEditorService, editorService); + + accessor = instantiationService.createInstance(ServiceAccessor); + + await part.whenRestored; + + const tracker = instantiationService.createInstance(TestBackupTracker); + + return [accessor, part, tracker]; + } + + test.skip('Track backups (untitled)', async function () { // {{SQL CARBON EDIT}} tabcolorfailure + this.timeout(20000); + + const [accessor, part, tracker] = await createTracker(); + + const untitledEditor = accessor.textFileService.untitled.create(); + await accessor.editorService.openEditor(untitledEditor, { pinned: true }); + + const untitledModel = await untitledEditor.resolve(); + untitledModel.textEditorModel.setValue('Super Good'); + + await accessor.backupFileService.joinBackupResource(); + + assert.equal(accessor.backupFileService.hasBackupSync(untitledEditor.getResource()), true); + + untitledModel.dispose(); + + await accessor.backupFileService.joinDiscardBackup(); + + assert.equal(accessor.backupFileService.hasBackupSync(untitledEditor.getResource()), false); + + part.dispose(); + tracker.dispose(); + }); + + test.skip('Track backups (file)', async function () { // {{SQL CARBON EDIT}} tabcolorfailure + this.timeout(20000); + + const [accessor, part, tracker] = await createTracker(); + + const resource = toResource.call(this, '/path/index.txt'); + await accessor.editorService.openEditor({ resource, options: { pinned: true } }); + + const fileModel = accessor.textFileService.files.get(resource); + fileModel?.textEditorModel?.setValue('Super Good'); + + await accessor.backupFileService.joinBackupResource(); + + assert.equal(accessor.backupFileService.hasBackupSync(resource), true); + + fileModel?.dispose(); + + await accessor.backupFileService.joinDiscardBackup(); + + assert.equal(accessor.backupFileService.hasBackupSync(resource), false); + + part.dispose(); + tracker.dispose(); + }); +}); diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution.ts new file mode 100644 index 0000000000..fd71a02c0f --- /dev/null +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution.ts @@ -0,0 +1,266 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; +import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; +import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { WorkspaceEdit } from 'vs/editor/common/modes'; +import { BulkEditPane } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditPane'; +import { IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation, IViewsRegistry } from 'vs/workbench/common/views'; +import { localize } from 'vs/nls'; +import { ViewPaneContainer, ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { PaneCompositePanel } from 'vs/workbench/browser/panel'; +import { RawContextKey, IContextKeyService, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; +import { BulkEditPreviewProvider } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditPreview'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; +import { WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { URI } from 'vs/base/common/uri'; +import { MenuId, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; +import { IEditorInput } from 'vs/workbench/common/editor'; +import type { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; + +function getBulkEditPane(panelService: IPanelService): BulkEditPane | undefined { + let view: ViewPane | undefined; + const activePanel = panelService.openPanel(BulkEditPane.ID, true); + if (activePanel instanceof PaneCompositePanel) { + view = activePanel.getViewPaneContainer().getView(BulkEditPane.ID); + } + if (view instanceof BulkEditPane) { + return view; + } + return undefined; +} + +class UXState { + + private readonly _activePanel: string | undefined; + + constructor( + @IPanelService private readonly _panelService: IPanelService, + @IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService, + ) { + this._activePanel = _panelService.getActivePanel()?.getId(); + } + + restore(): void { + + // (1) restore previous panel + if (typeof this._activePanel === 'string') { + this._panelService.openPanel(this._activePanel); + } else { + this._panelService.hideActivePanel(); + } + + // (2) close preview editors + for (let group of this._editorGroupsService.groups) { + let previewEditors: IEditorInput[] = []; + for (let input of group.editors) { + + let resource: URI | undefined; + if (input instanceof DiffEditorInput) { + resource = input.modifiedInput.getResource(); + } else { + resource = input.getResource(); + } + + if (resource?.scheme === BulkEditPreviewProvider.Schema) { + previewEditors.push(input); + } + } + + if (previewEditors.length) { + group.closeEditors(previewEditors, { preserveFocus: true }); + } + } + } +} + +class PreviewSession { + constructor( + readonly uxState: UXState, + readonly cts: CancellationTokenSource = new CancellationTokenSource(), + ) { } +} + +class BulkEditPreviewContribution { + + static readonly ctxEnabled = new RawContextKey('refactorPreview.enabled', false); + + private readonly _ctxEnabled: IContextKey; + + private _activeSession: PreviewSession | undefined; + + constructor( + @IPanelService private readonly _panelService: IPanelService, + @IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService, + @IBulkEditService bulkEditService: IBulkEditService, + @IContextKeyService contextKeyService: IContextKeyService, + ) { + bulkEditService.setPreviewHandler((edit) => this._previewEdit(edit)); + this._ctxEnabled = BulkEditPreviewContribution.ctxEnabled.bindTo(contextKeyService); + } + + private async _previewEdit(edit: WorkspaceEdit) { + this._ctxEnabled.set(true); + + // session + let session: PreviewSession; + if (this._activeSession) { + this._activeSession.cts.dispose(true); + session = new PreviewSession(this._activeSession.uxState); + } else { + session = new PreviewSession(new UXState(this._panelService, this._editorGroupsService)); + } + this._activeSession = session; + + // the actual work... + try { + const view = getBulkEditPane(this._panelService); + if (!view) { + return edit; + } + + const newEditOrUndefined = await view.setInput(edit, session.cts.token); + if (!newEditOrUndefined) { + return { edits: [] }; + } + + return newEditOrUndefined; + + } finally { + // restore UX state + if (this._activeSession === session) { + this._activeSession.uxState.restore(); + this._activeSession.cts.dispose(); + this._ctxEnabled.set(false); + this._activeSession = undefined; + } + } + } +} + + +// CMD: accept +registerAction2(class ApplyAction extends Action2 { + + constructor() { + super({ + id: 'refactorPreview.apply', + title: { value: localize('apply', "Apply Refactoring"), original: 'Apply Refactoring' }, + category: localize('cat', "Refactor Preview"), + icon: { id: 'codicon/check' }, + precondition: BulkEditPreviewContribution.ctxEnabled, + menu: [{ + id: MenuId.BulkEditTitle, + group: 'navigation' + }, { + id: MenuId.BulkEditContext, + order: 1 + }], + keybinding: { + weight: KeybindingWeight.EditorContrib - 10, + when: ContextKeyExpr.and(BulkEditPreviewContribution.ctxEnabled, ContextKeyExpr.equals('activePanel', BulkEditPane.ID)), + primary: KeyMod.Shift + KeyCode.Enter, + } + }); + } + + run(accessor: ServicesAccessor): any { + const panelService = accessor.get(IPanelService); + const view = getBulkEditPane(panelService); + if (view) { + view.accept(); + } + } +}); + +// CMD: discard +registerAction2(class DiscardAction extends Action2 { + + constructor() { + super({ + id: 'refactorPreview.discard', + title: { value: localize('Discard', "Discard Refactoring"), original: 'Discard Refactoring' }, + category: localize('cat', "Refactor Preview"), + icon: { id: 'codicon/clear-all' }, + precondition: BulkEditPreviewContribution.ctxEnabled, + menu: [{ + id: MenuId.BulkEditTitle, + group: 'navigation' + }, { + id: MenuId.BulkEditContext, + order: 2 + }] + }); + } + + run(accessor: ServicesAccessor): void | Promise { + const panelService = accessor.get(IPanelService); + const view = getBulkEditPane(panelService); + if (view) { + view.discard(); + } + } +}); + + +// CMD: toggle +registerAction2(class ToggleAction extends Action2 { + + constructor() { + super({ + id: 'refactorPreview.toggleCheckedState', + title: { value: localize('toogleSelection', "Toggle Change"), original: 'Toggle Change' }, + category: localize('cat', "Refactor Preview"), + precondition: BulkEditPreviewContribution.ctxEnabled, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: WorkbenchListFocusContextKey, + primary: KeyCode.Space, + }, + menu: { + id: MenuId.BulkEditContext, + group: 'navigation' + } + }); + } + + run(accessor: ServicesAccessor): void | Promise { + const panelService = accessor.get(IPanelService); + const view = getBulkEditPane(panelService); + if (view) { + view.toggleChecked(); + } + } +}); + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution( + BulkEditPreviewContribution, LifecyclePhase.Ready +); + +const container = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ + id: BulkEditPane.ID, + name: localize('panel', "Refactor Preview"), + hideIfEmpty: true, + ctorDescriptor: new SyncDescriptor( + ViewPaneContainer, + [BulkEditPane.ID, BulkEditPane.ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }] + ) +}, ViewContainerLocation.Panel); + +Registry.as(ViewContainerExtensions.ViewsRegistry).registerViews([{ + id: BulkEditPane.ID, + name: localize('panel', "Refactor Preview"), + when: BulkEditPreviewContribution.ctxEnabled, + ctorDescriptor: new SyncDescriptor(BulkEditPane), +}], container); + diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.css b/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.css new file mode 100644 index 0000000000..0b8ef2999b --- /dev/null +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.css @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench .bulk-edit-panel .highlight.remove { + text-decoration: line-through; +} + +.monaco-workbench .bulk-edit-panel .message { + padding: 10px 20px +} + +.monaco-workbench .bulk-edit-panel [data-state="message"] .message, +.monaco-workbench .bulk-edit-panel [data-state="data"] .tree +{ + display: inherit; +} + +.monaco-workbench .bulk-edit-panel [data-state="data"] .message, +.monaco-workbench .bulk-edit-panel [data-state="message"] .tree +{ + display: none; +} + +.monaco-workbench .bulk-edit-panel .monaco-tl-contents { + display: flex; +} + +.monaco-workbench .bulk-edit-panel .monaco-tl-contents .edit-checkbox { + align-self: center; +} + +.monaco-workbench .bulk-edit-panel .monaco-tl-contents .edit-checkbox.disabled { + opacity: .5; +} + +.monaco-workbench .bulk-edit-panel .monaco-tl-contents .details { + margin-left: .5em; + opacity: .7; + font-size: 0.9em; + white-space: pre +} diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPane.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPane.ts new file mode 100644 index 0000000000..3d96654b29 --- /dev/null +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPane.ts @@ -0,0 +1,298 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./bulkEdit'; +import { WorkbenchAsyncDataTree, TreeResourceNavigator2, IOpenEvent } from 'vs/platform/list/browser/listService'; +import { WorkspaceEdit } from 'vs/editor/common/modes'; +import { BulkEditElement, BulkEditDelegate, TextEditElementRenderer, FileElementRenderer, BulkEditDataSource, BulkEditIdentityProvider, FileElement, TextEditElement, BulkEditAccessibilityProvider, BulkEditAriaProvider } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditTree'; +import { FuzzyScore } from 'vs/base/common/filters'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; +import { diffInserted, diffRemoved } from 'vs/platform/theme/common/colorRegistry'; +import { localize } from 'vs/nls'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { BulkEditPreviewProvider, BulkFileOperations, BulkFileOperationType } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditPreview'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { URI } from 'vs/base/common/uri'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; +import { ResourceLabels, IResourceLabelsContainer } from 'vs/workbench/browser/labels'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import Severity from 'vs/base/common/severity'; +import { basename } from 'vs/base/common/resources'; +import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { IAction } from 'vs/base/common/actions'; +import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; + +const enum State { + Data = 'data', + Message = 'message' +} + +export class BulkEditPane extends ViewPane { + + static readonly ID = 'refactorPreview'; + + private _tree!: WorkbenchAsyncDataTree; + private _message!: HTMLSpanElement; + + private readonly _disposables = new DisposableStore(); + private readonly _sessionDisposables = new DisposableStore(); + private _currentResolve?: (edit?: WorkspaceEdit) => void; + private _currentInput?: BulkFileOperations; + + constructor( + options: IViewletViewOptions, + @IInstantiationService private readonly _instaService: IInstantiationService, + @IEditorService private readonly _editorService: IEditorService, + @ILabelService private readonly _labelService: ILabelService, + @ITextModelService private readonly _textModelService: ITextModelService, + @IDialogService private readonly _dialogService: IDialogService, + @IMenuService private readonly _menuService: IMenuService, + @IContextMenuService private readonly _contextMenuService: IContextMenuService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IKeybindingService keybindingService: IKeybindingService, + @IContextMenuService contextMenuService: IContextMenuService, + @IConfigurationService configurationService: IConfigurationService, + ) { + super( + { ...options, titleMenuId: MenuId.BulkEditTitle }, + keybindingService, contextMenuService, configurationService, _contextKeyService, _instaService + ); + + this.element.classList.add('bulk-edit-panel', 'show-file-icons'); + } + + dispose(): void { + this._tree.dispose(); + this._disposables.dispose(); + } + + protected renderBody(parent: HTMLElement): void { + + const resourceLabels = this._instaService.createInstance( + ResourceLabels, + { onDidChangeVisibility: this.onDidChangeBodyVisibility } + ); + this._disposables.add(resourceLabels); + + // tree + const treeContainer = document.createElement('div'); + treeContainer.className = 'tree'; + treeContainer.style.width = '100%'; + treeContainer.style.height = '100%'; + parent.appendChild(treeContainer); + + this._tree = this._instaService.createInstance( + WorkbenchAsyncDataTree, this.id, treeContainer, + new BulkEditDelegate(), + [new TextEditElementRenderer(), this._instaService.createInstance(FileElementRenderer, resourceLabels)], + this._instaService.createInstance(BulkEditDataSource), + { + accessibilityProvider: this._instaService.createInstance(BulkEditAccessibilityProvider), + ariaProvider: new BulkEditAriaProvider(), + identityProvider: new BulkEditIdentityProvider(), + expandOnlyOnTwistieClick: true, + multipleSelectionSupport: false + } + ); + + this._disposables.add(this._tree.onContextMenu(this._onContextMenu, this)); + + const navigator = new TreeResourceNavigator2(this._tree, { openOnFocus: true }); + this._disposables.add(navigator); + this._disposables.add(navigator.onDidOpenResource(e => this._openElementAsEditor(e))); + + // message + this._message = document.createElement('span'); + this._message.className = 'message'; + this._message.innerText = localize('empty.msg', "Invoke a code action, like rename, to see a preview of its changes here."); + parent.appendChild(this._message); + + // + this._setState(State.Message); + } + + protected layoutBody(height: number, width: number): void { + this._tree.layout(height, width); + } + + private _setState(state: State): void { + this.element.dataset['state'] = state; + } + + async setInput(edit: WorkspaceEdit, token: CancellationToken): Promise { + this._setState(State.Data); + this._sessionDisposables.clear(); + + if (this._currentResolve) { + this._currentResolve(undefined); + this._currentResolve = undefined; + } + + const input = await this._instaService.invokeFunction(BulkFileOperations.create, edit); + const provider = this._instaService.createInstance(BulkEditPreviewProvider, input); + this._sessionDisposables.add(provider); + this._sessionDisposables.add(input); + + this._currentInput = input; + + return new Promise(async resolve => { + + token.onCancellationRequested(() => resolve()); + + this._currentResolve = resolve; + await this._tree.setInput(input); + this._tree.domFocus(); + this._tree.focusFirst(); + + // this._tree.expandAll() workaround + for (let node of this._tree.getNode(input).children) { + if (node.element instanceof FileElement) { + this._tree.expand(node.element, false); + } + } + + // refresh when check state changes + this._sessionDisposables.add(input.onDidChangeCheckedState(() => { + this._tree.updateChildren(); + })); + }); + } + + accept(): void { + + const conflicts = this._currentInput?.conflicts.list(); + + if (!conflicts || conflicts.length === 0) { + this._done(true); + return; + } + + let message: string; + if (conflicts.length === 1) { + message = localize('conflict.1', "Cannot apply refactoring because '{0}' has changed in the meantime.", this._labelService.getUriLabel(conflicts[0], { relative: true })); + } else { + message = localize('conflict.N', "Cannot apply refactoring because {0} other files have changed in the meantime.", conflicts.length); + } + + this._dialogService.show(Severity.Warning, message, []).finally(() => this._done(false)); + } + + discard() { + this._done(false); + } + + toggleChecked() { + const [first] = this._tree.getFocus(); + if (first) { + first.edit.updateChecked(!first.edit.isChecked()); + } + } + + private _done(accept: boolean): void { + if (this._currentResolve) { + this._currentResolve(accept ? this._currentInput?.asWorkspaceEdit() : undefined); + this._currentInput = undefined; + } + this._setState(State.Message); + this._sessionDisposables.clear(); + } + + private async _openElementAsEditor(e: IOpenEvent): Promise { + type Mutable = { + -readonly [P in keyof T]: T[P] + }; + + let options: Mutable = { ...e.editorOptions }; + let fileElement: FileElement; + if (e.element instanceof TextEditElement) { + fileElement = e.element.parent; + options.selection = e.element.edit.edit.range; + + } else if (e.element instanceof FileElement) { + fileElement = e.element; + options.selection = e.element.edit.textEdits[0]?.edit.range; + + } else { + // invalid event + return; + } + + let leftResource: URI | undefined; + if (fileElement.edit.type & BulkFileOperationType.TextEdit) { + try { + (await this._textModelService.createModelReference(fileElement.uri)).dispose(); + leftResource = fileElement.uri; + } catch { + leftResource = BulkEditPreviewProvider.emptyPreview; + } + } + + const previewUri = BulkEditPreviewProvider.asPreviewUri(fileElement.uri); + + if (leftResource) { + // show diff editor + this._editorService.openEditor({ + leftResource, + rightResource: previewUri, + label: localize('edt.title', "{0} (refactor preview)", basename(fileElement.uri)), + options + }); + } else { + // show 'normal' editor + let typeLabel: string | undefined; + if (fileElement.edit.type & BulkFileOperationType.Rename) { + typeLabel = localize('rename', "rename"); + } else if (fileElement.edit.type & BulkFileOperationType.Create) { + typeLabel = localize('create', "create"); + } else if (fileElement.edit.type & BulkFileOperationType.Delete) { + typeLabel = localize('delete', "delete"); + } + + this._editorService.openEditor({ + label: typeLabel && localize('edt.title2', "{0} ({1}, refactor preview)", basename(fileElement.uri), typeLabel), + resource: previewUri, + options + }); + } + } + + private _onContextMenu(e: ITreeContextMenuEvent): void { + const menu = this._menuService.createMenu(MenuId.BulkEditContext, this._contextKeyService); + const actions: IAction[] = []; + const disposable = createAndFillInContextMenuActions(menu, undefined, actions, this._contextMenuService); + + this._contextMenuService.showContextMenu({ + getActions: () => actions, + getAnchor: () => e.anchor, + onHide: () => { + disposable.dispose(); + menu.dispose(); + } + }); + } +} + +registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { + + const diffInsertedColor = theme.getColor(diffInserted); + if (diffInsertedColor) { + collector.addRule(`.monaco-workbench .bulk-edit-panel .highlight.insert { background-color: ${diffInsertedColor}; }`); + } + const diffRemovedColor = theme.getColor(diffRemoved); + if (diffRemovedColor) { + collector.addRule(`.monaco-workbench .bulk-edit-panel .highlight.remove { background-color: ${diffRemovedColor}; }`); + } +}); diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPreview.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPreview.ts new file mode 100644 index 0000000000..a80f34b1ec --- /dev/null +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPreview.ts @@ -0,0 +1,324 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService'; +import { URI } from 'vs/base/common/uri'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; +import { WorkspaceEdit, TextEdit, WorkspaceTextEdit, WorkspaceFileEdit } from 'vs/editor/common/modes'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { mergeSort, coalesceInPlace } from 'vs/base/common/arrays'; +import { Range } from 'vs/editor/common/core/range'; +import { EditOperation } from 'vs/editor/common/core/editOperation'; +import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IFileService } from 'vs/platform/files/common/files'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; +import { ConflictDetector } from 'vs/workbench/services/bulkEdit/browser/conflicts'; + +class CheckedObject { + + private _checked: boolean = true; + + constructor(protected _emitter: Emitter) { } + + updateChecked(checked: boolean) { + if (this._checked !== checked) { + this._checked = checked; + this._emitter.fire(this); + } + } + + isChecked(): boolean { + return this._checked; + } +} + +export class BulkTextEdit extends CheckedObject { + + constructor( + readonly parent: BulkFileOperation, + readonly edit: TextEdit, + emitter: Emitter + ) { + super(emitter); + } +} + +export const enum BulkFileOperationType { + TextEdit = 1, + Create = 2, + Delete = 4, + Rename = 8, +} + +export class BulkFileOperation extends CheckedObject { + + type: BulkFileOperationType = 0; + textEdits: BulkTextEdit[] = []; + originalEdits = new Map(); + newUri?: URI; + + constructor( + readonly uri: URI, + readonly parent: BulkFileOperations + ) { + super(parent._onDidChangeCheckedState); + } + + addEdit(index: number, type: BulkFileOperationType, edit: WorkspaceTextEdit | WorkspaceFileEdit, ) { + this.type += type; + this.originalEdits.set(index, edit); + if (WorkspaceTextEdit.is(edit)) { + this.textEdits = this.textEdits.concat(edit.edits.map(edit => new BulkTextEdit(this, edit, this._emitter))); + + } else if (type === BulkFileOperationType.Rename) { + this.newUri = edit.newUri; + } + } +} + +export class BulkFileOperations { + + static async create(accessor: ServicesAccessor, bulkEdit: WorkspaceEdit): Promise { + const result = accessor.get(IInstantiationService).createInstance(BulkFileOperations, bulkEdit); + return await result._init(); + } + + readonly _onDidChangeCheckedState = new Emitter(); + readonly onDidChangeCheckedState: Event = this._onDidChangeCheckedState.event; + + readonly fileOperations: BulkFileOperation[] = []; + + readonly conflicts: ConflictDetector; + + constructor( + private readonly _bulkEdit: WorkspaceEdit, + @IFileService private readonly _fileService: IFileService, + @IInstantiationService instaService: IInstantiationService, + ) { + this.conflicts = instaService.createInstance(ConflictDetector, _bulkEdit); + } + + dispose(): void { + this.conflicts.dispose(); + } + + async _init() { + const operationByResource = new Map(); + const newToOldUri = new Map(); + + for (let idx = 0; idx < this._bulkEdit.edits.length; idx++) { + const edit = this._bulkEdit.edits[idx]; + + let uri: URI; + let type: BulkFileOperationType; + + if (WorkspaceTextEdit.is(edit)) { + type = BulkFileOperationType.TextEdit; + uri = edit.resource; + + } else if (edit.newUri && edit.oldUri) { + type = BulkFileOperationType.Rename; + uri = edit.oldUri; + if (edit.options?.overwrite === undefined && edit.options?.ignoreIfExists && await this._fileService.exists(uri)) { + // noop -> "soft" rename to something that already exists + continue; + } + // map newUri onto oldUri so that text-edit appear for + // the same file element + newToOldUri.set(edit.newUri.toString(), uri.toString()); + + } else if (edit.oldUri) { + type = BulkFileOperationType.Delete; + uri = edit.oldUri; + if (edit.options?.ignoreIfNotExists && !await this._fileService.exists(uri)) { + // noop -> "soft" delete something that doesn't exist + continue; + } + + } else if (edit.newUri) { + type = BulkFileOperationType.Create; + uri = edit.newUri; + if (edit.options?.overwrite === undefined && edit.options?.ignoreIfExists && await this._fileService.exists(uri)) { + // noop -> "soft" create something that already exists + continue; + } + + } else { + // invalid edit -> skip + continue; + } + + let key = uri.toString(); + let operation = operationByResource.get(key); + + // rename + if (!operation && newToOldUri.has(key)) { + key = newToOldUri.get(key)!; + operation = operationByResource.get(key); + } + + if (!operation) { + operation = new BulkFileOperation(uri, this); + operationByResource.set(key, operation); + } + operation.addEdit(idx, type, edit); + } + + operationByResource.forEach(value => this.fileOperations.push(value)); + return this; + } + + asWorkspaceEdit(): WorkspaceEdit { + const result: WorkspaceEdit = { edits: [] }; + let allAccepted = true; + for (let file of this.fileOperations) { + + if (!file.isChecked()) { + allAccepted = false; + continue; + } + + const keyOfEdit = (edit: TextEdit) => JSON.stringify(edit); + const checkedEdits = new Set(); + + for (let edit of file.textEdits) { + if (edit.isChecked()) { + checkedEdits.add(keyOfEdit(edit.edit)); + } + } + + file.originalEdits.forEach((value, idx) => { + + if (WorkspaceTextEdit.is(value)) { + let newValue: WorkspaceTextEdit = { ...value, edits: [] }; + let allEditsAccepted = true; + for (let edit of value.edits) { + if (!checkedEdits.has(keyOfEdit(edit))) { + allEditsAccepted = false; + } else { + newValue.edits.push(edit); + } + } + if (!allEditsAccepted) { + value = newValue; + allAccepted = false; + } + } + + result.edits[idx] = value; + }); + } + if (!allAccepted) { + // only return a new edit when something has changed + coalesceInPlace(result.edits); + return result; + } + return this._bulkEdit; + + } +} + +export class BulkEditPreviewProvider implements ITextModelContentProvider { + + static readonly Schema = 'vscode-bulkeditpreview'; + + static emptyPreview = URI.from({ scheme: BulkEditPreviewProvider.Schema, fragment: 'empty' }); + + static asPreviewUri(uri: URI): URI { + return URI.from({ scheme: BulkEditPreviewProvider.Schema, path: uri.path, query: uri.toString() }); + } + + static fromPreviewUri(uri: URI): URI { + return URI.parse(uri.query); + } + + private readonly _disposables = new DisposableStore(); + private readonly _ready: Promise; + private readonly _modelPreviewEdits = new Map(); + + constructor( + private readonly _operations: BulkFileOperations, + @IModeService private readonly _modeService: IModeService, + @IModelService private readonly _modelService: IModelService, + @ITextModelService private readonly _textModelResolverService: ITextModelService + ) { + this._disposables.add(this._textModelResolverService.registerTextModelContentProvider(BulkEditPreviewProvider.Schema, this)); + this._ready = this._init(); + } + + dispose(): void { + this._disposables.dispose(); + } + + private async _init() { + for (let operation of this._operations.fileOperations) { + await this._applyTextEditsToPreviewModel(operation); + } + this._disposables.add(this._operations.onDidChangeCheckedState(element => { + let operation = element instanceof BulkFileOperation ? element : element.parent; + this._applyTextEditsToPreviewModel(operation); + })); + } + + private async _applyTextEditsToPreviewModel(operation: BulkFileOperation) { + const model = await this._getOrCreatePreviewModel(operation.uri); + + // undo edits that have been done before + let undoEdits = this._modelPreviewEdits.get(model.id); + if (undoEdits) { + model.applyEdits(undoEdits); + } + // compute new edits + const newEdits = mergeSort( + operation.textEdits.filter(edit => edit.isChecked() && edit.parent.isChecked()).map(edit => EditOperation.replaceMove(Range.lift(edit.edit.range), edit.edit.text)), + (a, b) => Range.compareRangesUsingStarts(a.range, b.range) + ); + // apply edits and keep undo edits + undoEdits = model.applyEdits(newEdits); + this._modelPreviewEdits.set(model.id, undoEdits); + } + + private async _getOrCreatePreviewModel(uri: URI) { + const previewUri = BulkEditPreviewProvider.asPreviewUri(uri); + let model = this._modelService.getModel(previewUri); + if (!model) { + try { + // try: copy existing + const ref = await this._textModelResolverService.createModelReference(uri); + const sourceModel = ref.object.textEditorModel; + model = this._modelService.createModel( + createTextBufferFactoryFromSnapshot(sourceModel.createSnapshot()), + this._modeService.create(sourceModel.getLanguageIdentifier().language), + previewUri + ); + ref.dispose(); + + } catch { + // create NEW model + model = this._modelService.createModel( + '', + this._modeService.createByFilepathOrFirstLine(previewUri), + previewUri + ); + } + // this is a little weird but otherwise editors and other cusomers + // will dispose my models before they should be disposed... + // And all of this is off the eventloop to prevent endless recursion + new Promise(async () => this._disposables.add(await this._textModelResolverService.createModelReference(model!.uri))); + } + return model; + } + + async provideTextContent(previewUri: URI) { + if (previewUri.toString() === BulkEditPreviewProvider.emptyPreview.toString()) { + return this._modelService.createModel('', null, previewUri); + } + await this._ready; + return this._modelService.getModel(previewUri); + } +} diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditTree.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditTree.ts new file mode 100644 index 0000000000..6d37337f27 --- /dev/null +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditTree.ts @@ -0,0 +1,379 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IAsyncDataSource, ITreeRenderer, ITreeNode } from 'vs/base/browser/ui/tree/tree'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { FuzzyScore, createMatches } from 'vs/base/common/filters'; +import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; +import { URI } from 'vs/base/common/uri'; +import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; +import { IIdentityProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { Range } from 'vs/editor/common/core/range'; +import * as dom from 'vs/base/browser/dom'; +import { ITextModel } from 'vs/editor/common/model'; +import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { TextModel } from 'vs/editor/common/model/textModel'; +import { BulkFileOperations, BulkFileOperation, BulkFileOperationType, BulkTextEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditPreview'; +import { FileKind } from 'vs/platform/files/common/files'; +import { localize } from 'vs/nls'; +import { ILabelService } from 'vs/platform/label/common/label'; +import type { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; +import type { IAriaProvider } from 'vs/base/browser/ui/list/listView'; + +// --- VIEW MODEL + +export class FileElement { + + readonly uri: URI; + + constructor(readonly edit: BulkFileOperation) { + this.uri = edit.uri; + } +} + +export class TextEditElement { + + constructor( + readonly parent: FileElement, + readonly edit: BulkTextEdit, + readonly prefix: string, readonly selecting: string, readonly inserting: string, readonly suffix: string + ) { } +} + +export type BulkEditElement = FileElement | TextEditElement; + +// --- DATA SOURCE + +export class BulkEditDataSource implements IAsyncDataSource { + + constructor(@ITextModelService private readonly _textModelService: ITextModelService) { } + + hasChildren(element: BulkFileOperations | BulkEditElement): boolean { + if (element instanceof FileElement) { + return element.edit.textEdits.length > 0; + } + if (element instanceof TextEditElement) { + return false; + } + return true; + } + + async getChildren(element: BulkFileOperations | BulkEditElement): Promise { + + // root -> file/text edits + if (element instanceof BulkFileOperations) { + return element.fileOperations.map(op => new FileElement(op)); + } + + // file: text edit + if (element instanceof FileElement && element.edit.textEdits.length > 0) { + // const previewUri = BulkEditPreviewProvider.asPreviewUri(element.edit.resource); + let textModel: ITextModel; + let textModelDisposable: IDisposable; + try { + const ref = await this._textModelService.createModelReference(element.edit.uri); + textModel = ref.object.textEditorModel; + textModelDisposable = ref; + } catch { + textModel = TextModel.createFromString(''); + textModelDisposable = textModel; + } + + const result = element.edit.textEdits.map(edit => { + const range = Range.lift(edit.edit.range); + + const tokens = textModel.getLineTokens(range.endLineNumber); + let suffixLen = 0; + for (let idx = tokens.findTokenIndexAtOffset(range.endColumn); suffixLen < 50 && idx < tokens.getCount(); idx++) { + suffixLen += tokens.getEndOffset(idx) - tokens.getStartOffset(idx); + } + + return new TextEditElement( + element, + edit, + textModel.getValueInRange(new Range(range.startLineNumber, 1, range.startLineNumber, range.startColumn)), // line start to edit start, + textModel.getValueInRange(range), + edit.edit.text, + textModel.getValueInRange(new Range(range.endLineNumber, range.endColumn, range.endLineNumber, range.endColumn + suffixLen)) + ); + }); + + textModelDisposable.dispose(); + return result; + } + + return []; + } +} + +// --- ACCESSI + +export class BulkEditAccessibilityProvider implements IAccessibilityProvider { + + constructor(@ILabelService private readonly _labelService: ILabelService) { } + + getAriaLabel(element: BulkEditElement): string | null { + if (element instanceof FileElement) { + if (element.edit.textEdits.length > 0) { + if (element.edit.type & BulkFileOperationType.Rename && element.edit.newUri) { + return localize( + 'aria.renameAndEdit', "Renaming {0} to {1}, also making text edits", + this._labelService.getUriLabel(element.edit.uri, { relative: true }), this._labelService.getUriLabel(element.edit.newUri, { relative: true }) + ); + + } else if (element.edit.type & BulkFileOperationType.Create) { + return localize( + 'aria.createAndEdit', "Creating {0}, also making text edits", + this._labelService.getUriLabel(element.edit.uri, { relative: true }) + ); + + } else if (element.edit.type & BulkFileOperationType.Delete) { + return localize( + 'aria.deleteAndEdit', "Deleting {0}, also making text edits", + this._labelService.getUriLabel(element.edit.uri, { relative: true }), + ); + } else { + return localize( + 'aria.editOnly', "{0}, making text edits", + this._labelService.getUriLabel(element.edit.uri, { relative: true }), + ); + } + + } else { + if (element.edit.type & BulkFileOperationType.Rename && element.edit.newUri) { + return localize( + 'aria.rename', "Renaming {0} to {1}", + this._labelService.getUriLabel(element.edit.uri, { relative: true }), this._labelService.getUriLabel(element.edit.newUri, { relative: true }) + ); + + } else if (element.edit.type & BulkFileOperationType.Create) { + return localize( + 'aria.create', "Creating {0}", + this._labelService.getUriLabel(element.edit.uri, { relative: true }) + ); + + } else if (element.edit.type & BulkFileOperationType.Delete) { + return localize( + 'aria.delete', "Deleting {0}", + this._labelService.getUriLabel(element.edit.uri, { relative: true }), + ); + } + } + } + + if (element instanceof TextEditElement) { + if (element.selecting.length > 0 && element.inserting.length > 0) { + // edit: replace + return localize('aria.replace', "line {0}, replacing {1} with {2}", element.edit.edit.range.startLineNumber, element.selecting, element.inserting); + } else if (element.selecting.length > 0 && element.inserting.length === 0) { + // edit: delete + return localize('aria.del', "line {0}, removing {1}", element.edit.edit.range.startLineNumber, element.selecting); + } else if (element.selecting.length === 0 && element.inserting.length > 0) { + // edit: insert + return localize('aria.insert', "line {0}, inserting {1}", element.edit.edit.range.startLineNumber, element.selecting); + } + } + + return null; + } +} + +// --- IDENT + +export class BulkEditIdentityProvider implements IIdentityProvider { + + getId(element: BulkEditElement): { toString(): string; } { + if (element instanceof FileElement) { + return element.uri; + } else { + return element.parent.uri.toString() + JSON.stringify(element.edit.edit); + } + } +} + +export class BulkEditAriaProvider implements IAriaProvider { + + getSetSize(_element: BulkEditElement, _index: number, listLength: number): number { + return listLength; + } + + getPosInSet(_element: BulkEditElement, index: number): number { + return index; + } + + getRole?(_element: BulkEditElement): string { + return 'checkbox'; + } +} + +// --- RENDERER + +class FileElementTemplate { + + private readonly _disposables = new DisposableStore(); + private readonly _localDisposables = new DisposableStore(); + + private readonly _checkbox: HTMLInputElement; + private readonly _label: IResourceLabel; + private readonly _details: HTMLSpanElement; + + constructor( + container: HTMLElement, + resourceLabels: ResourceLabels, + @ILabelService private readonly _labelService: ILabelService, + ) { + + this._checkbox = document.createElement('input'); + this._checkbox.className = 'edit-checkbox'; + this._checkbox.type = 'checkbox'; + this._checkbox.setAttribute('role', 'checkbox'); + container.appendChild(this._checkbox); + + this._label = resourceLabels.create(container, { supportHighlights: true }); + + this._details = document.createElement('span'); + this._details.className = 'details'; + container.appendChild(this._details); + } + + dispose(): void { + this._localDisposables.dispose(); + this._disposables.dispose(); + this._label.dispose(); + } + + set(element: FileElement, score: FuzzyScore | undefined) { + this._localDisposables.clear(); + this._localDisposables.add(dom.addDisposableListener(this._checkbox, 'change', (() => element.edit.updateChecked(this._checkbox.checked)))); + this._checkbox.checked = element.edit.isChecked(); + + if (element.edit.type & BulkFileOperationType.Rename && element.edit.newUri) { + // rename: NEW NAME (old name) + this._label.setFile(element.edit.newUri, { + matches: createMatches(score), + fileKind: FileKind.FILE, + fileDecorations: { colors: true, badges: false }, + }); + + this._details.innerText = localize( + 'detail.rename', "(renaming from {0})", + this._labelService.getUriLabel(element.uri, { relative: true }) + ); + + } else { + // create, delete, edit: NAME + this._label.setFile(element.uri, { + matches: createMatches(score), + fileKind: FileKind.FILE, + fileDecorations: { colors: true, badges: false }, + }); + + if (element.edit.type & BulkFileOperationType.Create) { + this._details.innerText = localize('detail.create', "(creating)"); + } else if (element.edit.type & BulkFileOperationType.Delete) { + this._details.innerText = localize('detail.del', "(deleting)"); + } else { + this._details.innerText = ''; + } + } + } +} + +export class FileElementRenderer implements ITreeRenderer { + + static readonly id: string = 'FileElementRenderer'; + + readonly templateId: string = FileElementRenderer.id; + + constructor( + private readonly _resourceLabels: ResourceLabels, + @ILabelService private readonly _labelService: ILabelService, + ) { } + + renderTemplate(container: HTMLElement): FileElementTemplate { + return new FileElementTemplate(container, this._resourceLabels, this._labelService); + } + + renderElement(node: ITreeNode, _index: number, template: FileElementTemplate): void { + template.set(node.element, node.filterData); + } + + disposeTemplate(template: FileElementTemplate): void { + template.dispose(); + } +} + +class TextEditElementTemplate { + + private readonly _disposables = new DisposableStore(); + private readonly _localDisposables = new DisposableStore(); + + private readonly _checkbox: HTMLInputElement; + private readonly _label: HighlightedLabel; + + constructor(container: HTMLElement) { + this._checkbox = document.createElement('input'); + this._checkbox.className = 'edit-checkbox'; + this._checkbox.type = 'checkbox'; + this._checkbox.setAttribute('role', 'checkbox'); + container.appendChild(this._checkbox); + + this._label = new HighlightedLabel(container, false); + dom.addClass(this._label.element, 'textedit'); + } + + dispose(): void { + this._localDisposables.dispose(); + this._disposables.dispose(); + } + + set(element: TextEditElement) { + this._localDisposables.clear(); + this._localDisposables.add(dom.addDisposableListener(this._checkbox, 'change', () => element.edit.updateChecked(this._checkbox.checked))); + this._checkbox.checked = element.edit.isChecked(); + dom.toggleClass(this._checkbox, 'disabled', !element.edit.parent.isChecked()); + + let value = ''; + value += element.prefix; + value += element.selecting; + value += element.inserting; + value += element.suffix; + + let selectHighlight: IHighlight = { start: element.prefix.length, end: element.prefix.length + element.selecting.length, extraClasses: 'remove' }; + let insertHighlight: IHighlight = { start: selectHighlight.end, end: selectHighlight.end + element.inserting.length, extraClasses: 'insert' }; + + this._label.set(value, [selectHighlight, insertHighlight], undefined, true); + } +} + +export class TextEditElementRenderer implements ITreeRenderer { + + static readonly id = 'TextEditElementRenderer'; + + readonly templateId: string = TextEditElementRenderer.id; + + renderTemplate(container: HTMLElement): TextEditElementTemplate { + return new TextEditElementTemplate(container); + } + + renderElement({ element }: ITreeNode, _index: number, template: TextEditElementTemplate): void { + template.set(element); + } + + disposeTemplate(_template: TextEditElementTemplate): void { } +} + +export class BulkEditDelegate implements IListVirtualDelegate { + + getHeight(): number { + return 23; + } + + getTemplateId(element: BulkEditElement): string { + return element instanceof FileElement + ? FileElementRenderer.id + : TextEditElementRenderer.id; + } +} diff --git a/src/vs/workbench/contrib/codeActions/common/codeActions.contribution.ts b/src/vs/workbench/contrib/codeActions/common/codeActions.contribution.ts index 3ab3f93e96..3158b15d19 100644 --- a/src/vs/workbench/contrib/codeActions/common/codeActions.contribution.ts +++ b/src/vs/workbench/contrib/codeActions/common/codeActions.contribution.ts @@ -4,27 +4,30 @@ *--------------------------------------------------------------------------------------------*/ import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; -import { CodeActionWorkbenchContribution, editorConfiguration } from 'vs/workbench/contrib/codeActions/common/configuration'; -import { CodeActionsExtensionPoint, codeActionsExtensionPointDescriptor } from 'vs/workbench/contrib/codeActions/common/extensionPoint'; +import { CodeActionsContribution, editorConfiguration } from 'vs/workbench/contrib/codeActions/common/codeActionsContribution'; +import { CodeActionsExtensionPoint, codeActionsExtensionPointDescriptor } from 'vs/workbench/contrib/codeActions/common/codeActionsExtensionPoint'; +import { CodeActionDocumentationContribution } from 'vs/workbench/contrib/codeActions/common/documentationContribution'; +import { DocumentationExtensionPoint, documentationExtensionPointDescriptor } from 'vs/workbench/contrib/codeActions/common/documentationExtensionPoint'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; const codeActionsExtensionPoint = ExtensionsRegistry.registerExtensionPoint(codeActionsExtensionPointDescriptor); +const documentationExtensionPoint = ExtensionsRegistry.registerExtensionPoint(documentationExtensionPointDescriptor); Registry.as(Extensions.Configuration) .registerConfiguration(editorConfiguration); -class WorkbenchContribution { +class WorkbenchConfigurationContribution { constructor( - @IKeybindingService keybindingsService: IKeybindingService, + @IInstantiationService instantiationService: IInstantiationService, ) { - // tslint:disable-next-line: no-unused-expression - new CodeActionWorkbenchContribution(codeActionsExtensionPoint, keybindingsService); + instantiationService.createInstance(CodeActionsContribution, codeActionsExtensionPoint); + instantiationService.createInstance(CodeActionDocumentationContribution, documentationExtensionPoint); } } Registry.as(WorkbenchExtensions.Workbench) - .registerWorkbenchContribution(WorkbenchContribution, LifecyclePhase.Eventually); + .registerWorkbenchContribution(WorkbenchConfigurationContribution, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/codeActions/common/configuration.ts b/src/vs/workbench/contrib/codeActions/common/codeActionsContribution.ts similarity index 91% rename from src/vs/workbench/contrib/codeActions/common/configuration.ts rename to src/vs/workbench/contrib/codeActions/common/codeActionsContribution.ts index 663de3dee4..f7efc9d15c 100644 --- a/src/vs/workbench/contrib/codeActions/common/configuration.ts +++ b/src/vs/workbench/contrib/codeActions/common/codeActionsContribution.ts @@ -15,7 +15,7 @@ import { Extensions, IConfigurationNode, IConfigurationRegistry, ConfigurationSc import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { CodeActionsExtensionPoint, ContributedCodeAction } from 'vs/workbench/contrib/codeActions/common/extensionPoint'; +import { CodeActionsExtensionPoint, ContributedCodeAction } from 'vs/workbench/contrib/codeActions/common/codeActionsExtensionPoint'; import { IExtensionPoint } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { editorConfigurationBaseNode } from 'vs/editor/common/config/commonEditorConfig'; @@ -40,17 +40,11 @@ const codeActionsOnSaveSchema: IConfigurationPropertySchema = { export const editorConfiguration = Object.freeze({ ...editorConfigurationBaseNode, properties: { - 'editor.codeActionsOnSave': codeActionsOnSaveSchema, - 'editor.codeActionsOnSaveTimeout': { - type: 'number', - default: 750, - description: nls.localize('codeActionsOnSaveTimeout', "Timeout in milliseconds after which the code actions that are run on save are cancelled."), - scope: ConfigurationScope.RESOURCE_LANGUAGE, - }, + 'editor.codeActionsOnSave': codeActionsOnSaveSchema } }); -export class CodeActionWorkbenchContribution extends Disposable implements IWorkbenchContribution { +export class CodeActionsContribution extends Disposable implements IWorkbenchContribution { private _contributedCodeActions: CodeActionsExtensionPoint[] = []; @@ -58,7 +52,7 @@ export class CodeActionWorkbenchContribution extends Disposable implements IWork constructor( codeActionsExtensionPoint: IExtensionPoint, - keybindingService: IKeybindingService, + @IKeybindingService keybindingService: IKeybindingService, ) { super(); diff --git a/src/vs/workbench/contrib/codeActions/common/extensionPoint.ts b/src/vs/workbench/contrib/codeActions/common/codeActionsExtensionPoint.ts similarity index 100% rename from src/vs/workbench/contrib/codeActions/common/extensionPoint.ts rename to src/vs/workbench/contrib/codeActions/common/codeActionsExtensionPoint.ts diff --git a/src/vs/workbench/contrib/codeActions/common/documentationContribution.ts b/src/vs/workbench/contrib/codeActions/common/documentationContribution.ts new file mode 100644 index 0000000000..ca9669fb83 --- /dev/null +++ b/src/vs/workbench/contrib/codeActions/common/documentationContribution.ts @@ -0,0 +1,84 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Range } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; +import { ITextModel } from 'vs/editor/common/model'; +import * as modes from 'vs/editor/common/modes'; +import { CodeActionKind } from 'vs/editor/contrib/codeAction/types'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IExtensionPoint } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { DocumentationExtensionPoint } from './documentationExtensionPoint'; + + +export class CodeActionDocumentationContribution extends Disposable implements IWorkbenchContribution, modes.CodeActionProvider { + + private contributions: { + title: string; + when: ContextKeyExpr; + command: string; + }[] = []; + + private readonly emptyCodeActionsList = { + actions: [], + dispose: () => { } + }; + + constructor( + extensionPoint: IExtensionPoint, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + ) { + super(); + + this._register(modes.CodeActionProviderRegistry.register('*', this)); + + extensionPoint.setHandler(points => { + this.contributions = []; + for (const documentation of points) { + if (!documentation.value.refactoring) { + continue; + } + + for (const contribution of documentation.value.refactoring) { + const precondition = ContextKeyExpr.deserialize(contribution.when); + if (!precondition) { + continue; + } + + this.contributions.push({ + title: contribution.title, + when: precondition, + command: contribution.command + }); + + } + } + }); + } + + async provideCodeActions(_model: ITextModel, _range: Range | Selection, context: modes.CodeActionContext, _token: CancellationToken): Promise { + return this.emptyCodeActionsList; + } + + public _getAdditionalMenuItems(context: modes.CodeActionContext, actions: readonly modes.CodeAction[]): modes.Command[] { + if (context.only !== CodeActionKind.Refactor.value) { + if (!actions.some(action => action.kind && CodeActionKind.Refactor.contains(new CodeActionKind(action.kind)))) { + return []; + } + } + + return this.contributions + .filter(contribution => this.contextKeyService.contextMatchesRules(contribution.when)) + .map(contribution => { + return { + id: contribution.command, + title: contribution.title + }; + }); + } +} diff --git a/src/vs/workbench/contrib/codeActions/common/documentationExtensionPoint.ts b/src/vs/workbench/contrib/codeActions/common/documentationExtensionPoint.ts new file mode 100644 index 0000000000..1edb65fa1b --- /dev/null +++ b/src/vs/workbench/contrib/codeActions/common/documentationExtensionPoint.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; +import { languagesExtPoint } from 'vs/workbench/services/mode/common/workbenchModeService'; + +export enum DocumentationExtensionPointFields { + when = 'when', + title = 'title', + command = 'command', +} + +export interface RefactoringDocumentationExtensionPoint { + readonly [DocumentationExtensionPointFields.title]: string; + readonly [DocumentationExtensionPointFields.when]: string; + readonly [DocumentationExtensionPointFields.command]: string; +} + +export interface DocumentationExtensionPoint { + readonly refactoring?: readonly RefactoringDocumentationExtensionPoint[]; +} + +const documentationExtensionPointSchema = Object.freeze({ + type: 'object', + description: nls.localize('contributes.documentation', "Contributed documentation."), + properties: { + 'refactoring': { + type: 'array', + description: nls.localize('contributes.documentation.refactorings', "Contributed documentation for refactorings."), + items: { + type: 'object', + description: nls.localize('contributes.documentation.refactoring', "Contributed documentation for refactoring."), + required: [ + DocumentationExtensionPointFields.title, + DocumentationExtensionPointFields.when, + DocumentationExtensionPointFields.command + ], + properties: { + [DocumentationExtensionPointFields.title]: { + type: 'string', + description: nls.localize('contributes.documentation.refactoring.title', "Label for the documentation used in the UI."), + }, + [DocumentationExtensionPointFields.when]: { + type: 'string', + description: nls.localize('contributes.documentation.refactoring.when', "When clause."), + }, + [DocumentationExtensionPointFields.command]: { + type: 'string', + description: nls.localize('contributes.documentation.refactoring.command', "Command executed."), + }, + }, + } + } + } +}); + +export const documentationExtensionPointDescriptor = { + extensionPoint: 'documentation', + deps: [languagesExtPoint], + jsonSchema: documentationExtensionPointSchema +}; diff --git a/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts b/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts index 43a1bc3718..38ae1273b4 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts @@ -8,7 +8,7 @@ import './accessibility/accessibility'; import './diffEditorHelper'; import './inspectKeybindings'; import './largeFileOptimizations'; -import './inspectTMScopes/inspectTMScopes'; +import './inspectEditorTokens/inspectEditorTokens'; import './toggleMinimap'; import './toggleMultiCursorModifier'; import './toggleRenderControlCharacter'; diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts index f009f85495..b11f8b23ce 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts @@ -54,7 +54,6 @@ export abstract class SimpleFindWidget extends Widget { return null; } try { - /* tslint:disable-next-line:no-unused-expression */ new RegExp(value); return null; } catch (e) { diff --git a/src/vs/workbench/contrib/codeEditor/browser/inspectTMScopes/inspectTMScopes.css b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.css similarity index 82% rename from src/vs/workbench/contrib/codeEditor/browser/inspectTMScopes/inspectTMScopes.css rename to src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.css index e2f08ab669..a34963aac3 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/inspectTMScopes/inspectTMScopes.css +++ b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.css @@ -3,37 +3,37 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.tm-inspect-widget { +.token-inspect-widget { z-index: 50; user-select: text; -webkit-user-select: text; padding: 10px; } -.tm-token { +.tiw-token { font-family: var(--monaco-monospace-font); } -.tm-metadata-separator { +.tiw-metadata-separator { height: 1px; border: 0; } -.tm-token-length { +.tiw-token-length { font-weight: normal; font-size: 60%; float: right; } -.tm-metadata-table { +.tiw-metadata-table { width: 100%; } -.tm-metadata-value { +.tiw-metadata-value { font-family: var(--monaco-monospace-font); text-align: right; } -.tm-theme-selector { +.tiw-theme-selector { font-family: var(--monaco-monospace-font); } diff --git a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts new file mode 100644 index 0000000000..fc1c7bc0eb --- /dev/null +++ b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts @@ -0,0 +1,474 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./inspectEditorTokens'; +import * as nls from 'vs/nls'; +import * as dom from 'vs/base/browser/dom'; +import { CharCode } from 'vs/base/common/charCode'; +import { Color } from 'vs/base/common/color'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { escape } from 'vs/base/common/strings'; +import { ContentWidgetPositionPreference, IActiveCodeEditor, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { ITextModel } from 'vs/editor/common/model'; +import { FontStyle, LanguageIdentifier, StandardTokenType, TokenMetadata, DocumentSemanticTokensProviderRegistry, SemanticTokensLegend, SemanticTokens } from 'vs/editor/common/modes'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { editorHoverBackground, editorHoverBorder } from 'vs/platform/theme/common/colorRegistry'; +import { HIGH_CONTRAST, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { findMatchingThemeRule } from 'vs/workbench/services/textMate/common/TMHelper'; +import { ITextMateService, IGrammar, IToken, StackElement } from 'vs/workbench/services/textMate/common/textMateService'; +import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; + +class InspectEditorTokensController extends Disposable implements IEditorContribution { + + public static readonly ID = 'editor.contrib.inspectEditorTokens'; + + public static get(editor: ICodeEditor): InspectEditorTokensController { + return editor.getContribution(InspectEditorTokensController.ID); + } + + private _editor: ICodeEditor; + private _textMateService: ITextMateService; + private _themeService: IWorkbenchThemeService; + private _modeService: IModeService; + private _notificationService: INotificationService; + private _widget: InspectEditorTokensWidget | null; + + constructor( + editor: ICodeEditor, + @ITextMateService textMateService: ITextMateService, + @IModeService modeService: IModeService, + @IWorkbenchThemeService themeService: IWorkbenchThemeService, + @INotificationService notificationService: INotificationService + ) { + super(); + this._editor = editor; + this._textMateService = textMateService; + this._themeService = themeService; + this._modeService = modeService; + this._notificationService = notificationService; + this._widget = null; + + this._register(this._editor.onDidChangeModel((e) => this.stop())); + this._register(this._editor.onDidChangeModelLanguage((e) => this.stop())); + this._register(this._editor.onKeyUp((e) => e.keyCode === KeyCode.Escape && this.stop())); + } + + public dispose(): void { + this.stop(); + super.dispose(); + } + + public launch(): void { + if (this._widget) { + return; + } + if (!this._editor.hasModel()) { + return; + } + this._widget = new InspectEditorTokensWidget(this._editor, this._textMateService, this._modeService, this._themeService, this._notificationService); + } + + public stop(): void { + if (this._widget) { + this._widget.dispose(); + this._widget = null; + } + } + + public toggle(): void { + if (!this._widget) { + this.launch(); + } else { + this.stop(); + } + } +} + +class InspectEditorTokens extends EditorAction { + + constructor() { + super({ + id: 'editor.action.inspectEditorTokens', + label: nls.localize('inspectEditorTokens', "Developer: Inspect Editor Tokens and Scopes"), + alias: 'Developer: Inspect Editor Tokens and Scopes', + precondition: undefined + }); + } + + public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + let controller = InspectEditorTokensController.get(editor); + if (controller) { + controller.toggle(); + } + } +} + +interface ITextMateTokenInfo { + token: IToken; + metadata: IDecodedMetadata; +} + +interface ISemanticTokenInfo { + type: string; + modifiers: string[]; + range: Range; + metadata: IDecodedMetadata +} + +interface IDecodedMetadata { + languageIdentifier: LanguageIdentifier; + tokenType: StandardTokenType; + fontStyle: string; + foreground?: string; + background?: string; +} + +function renderTokenText(tokenText: string): string { + if (tokenText.length > 40) { + tokenText = tokenText.substr(0, 20) + '…' + tokenText.substr(tokenText.length - 20); + } + let result: string = ''; + for (let charIndex = 0, len = tokenText.length; charIndex < len; charIndex++) { + let charCode = tokenText.charCodeAt(charIndex); + switch (charCode) { + case CharCode.Tab: + result += '→'; + break; + + case CharCode.Space: + result += '·'; + break; + + case CharCode.LessThan: + result += '<'; + break; + + case CharCode.GreaterThan: + result += '>'; + break; + + case CharCode.Ampersand: + result += '&'; + break; + + default: + result += String.fromCharCode(charCode); + } + } + return result; +} + +type SemanticTokensResult = { tokens: SemanticTokens, legend: SemanticTokensLegend }; + +class InspectEditorTokensWidget extends Disposable implements IContentWidget { + + private static readonly _ID = 'editor.contrib.inspectEditorTokensWidget'; + + // Editor.IContentWidget.allowEditorOverflow + public readonly allowEditorOverflow = true; + + private _isDisposed: boolean; + private readonly _editor: IActiveCodeEditor; + private readonly _modeService: IModeService; + private readonly _themeService: IWorkbenchThemeService; + private readonly _notificationService: INotificationService; + private readonly _model: ITextModel; + private readonly _domNode: HTMLElement; + private readonly _grammar: Promise; + private readonly _semanticTokens: Promise; + private readonly _currentRequestCancellationTokenSource: CancellationTokenSource; + + constructor( + editor: IActiveCodeEditor, + textMateService: ITextMateService, + modeService: IModeService, + themeService: IWorkbenchThemeService, + notificationService: INotificationService + ) { + super(); + this._isDisposed = false; + this._editor = editor; + this._modeService = modeService; + this._themeService = themeService; + this._notificationService = notificationService; + this._model = this._editor.getModel(); + this._domNode = document.createElement('div'); + this._domNode.className = 'token-inspect-widget'; + this._currentRequestCancellationTokenSource = new CancellationTokenSource(); + this._grammar = textMateService.createGrammar(this._model.getLanguageIdentifier().language); + this._semanticTokens = this._computeSemanticTokens(); + this._beginCompute(this._editor.getPosition()); + this._register(this._editor.onDidChangeCursorPosition((e) => this._beginCompute(this._editor.getPosition()))); + this._editor.addContentWidget(this); + } + + public dispose(): void { + this._isDisposed = true; + this._editor.removeContentWidget(this); + this._currentRequestCancellationTokenSource.cancel(); + super.dispose(); + } + + public getId(): string { + return InspectEditorTokensWidget._ID; + } + + private _beginCompute(position: Position): void { + dom.clearNode(this._domNode); + this._domNode.appendChild(document.createTextNode(nls.localize('inspectTMScopesWidget.loading', "Loading..."))); + + Promise.all([this._grammar, this._semanticTokens]).then(([grammar, semanticTokens]) => { + if (this._isDisposed) { + return; + } + let text = this._compute(grammar, semanticTokens, position); + this._domNode.innerHTML = text; + this._editor.layoutContentWidget(this); + }, (err) => { + this._notificationService.warn(err); + + setTimeout(() => { + InspectEditorTokensController.get(this._editor).stop(); + }); + }); + + } + + private _compute(grammar: IGrammar | null, semanticTokens: SemanticTokensResult | null, position: Position): string { + const textMateTokenInfo = grammar && this._getTokensAtPosition(grammar, position); + const semanticTokenInfo = semanticTokens && this._getSemanticTokenAtPosition(semanticTokens, position); + + let tokenText; + let metadata: IDecodedMetadata | undefined; + let primary: IDecodedMetadata | undefined; + if (textMateTokenInfo) { + let tokenStartIndex = textMateTokenInfo.token.startIndex; + let tokenEndIndex = textMateTokenInfo.token.endIndex; + tokenText = this._model.getLineContent(position.lineNumber).substring(tokenStartIndex, tokenEndIndex); + metadata = textMateTokenInfo.metadata; + primary = semanticTokenInfo?.metadata; + } else if (semanticTokenInfo) { + tokenText = this._model.getValueInRange(semanticTokenInfo.range); + metadata = semanticTokenInfo.metadata; + } else { + return 'No grammar or semantic tokens available.'; + } + + let result = ''; + result += `

${renderTokenText(tokenText)}(${tokenText.length} ${tokenText.length === 1 ? 'char' : 'chars'})

`; + result += ``; + + result += ``; + result += ``; + result += ``; + if (semanticTokenInfo) { + result += ``; + const modifiers = semanticTokenInfo.modifiers.join(' ') || '-'; + result += ``; + } + result += ``; + + result += ``; + result += ``; + result += this._formatMetadata(metadata, primary); + result += ``; + + if (textMateTokenInfo) { + let theme = this._themeService.getColorTheme(); + result += ``; + let matchingRule = findMatchingThemeRule(theme, textMateTokenInfo.token.scopes, false); + if (matchingRule) { + result += `${matchingRule.rawSelector}\n${JSON.stringify(matchingRule.settings, null, '\t')}`; + } else { + result += `No theme selector.`; + } + + result += `
    `; + for (let i = textMateTokenInfo.token.scopes.length - 1; i >= 0; i--) { + result += `
  • ${escape(textMateTokenInfo.token.scopes[i])}
  • `; + } + result += `
`; + } + return result; + } + + private _formatMetadata(metadata: IDecodedMetadata, master?: IDecodedMetadata) { + let result = ''; + + const fontStyle = master ? master.fontStyle : metadata.fontStyle; + result += `font style${fontStyle}`; + const foreground = master && master.foreground || metadata.foreground; + result += `foreground${foreground}`; + const background = master && master.background || metadata.background; + result += `background${background}`; + + if (foreground && background) { + const backgroundColor = Color.fromHex(background), foregroundColor = Color.fromHex(foreground); + + if (backgroundColor.isOpaque()) { + result += `contrast ratio${backgroundColor.getContrastRatio(foregroundColor.makeOpaque(backgroundColor)).toFixed(2)}`; + } else { + result += 'Contrast ratio cannot be precise for background colors that use transparency'; + } + } + return result; + } + + private _decodeMetadata(metadata: number): IDecodedMetadata { + let colorMap = this._themeService.getColorTheme().tokenColorMap; + let languageId = TokenMetadata.getLanguageId(metadata); + let tokenType = TokenMetadata.getTokenType(metadata); + let fontStyle = TokenMetadata.getFontStyle(metadata); + let foreground = TokenMetadata.getForeground(metadata); + let background = TokenMetadata.getBackground(metadata); + return { + languageIdentifier: this._modeService.getLanguageIdentifier(languageId)!, + tokenType: tokenType, + fontStyle: this._fontStyleToString(fontStyle), + foreground: colorMap[foreground], + background: colorMap[background] + }; + } + + private _tokenTypeToString(tokenType: StandardTokenType): string { + switch (tokenType) { + case StandardTokenType.Other: return 'Other'; + case StandardTokenType.Comment: return 'Comment'; + case StandardTokenType.String: return 'String'; + case StandardTokenType.RegEx: return 'RegEx'; + } + return '??'; + } + + private _fontStyleToString(fontStyle: FontStyle): string { + let r = ''; + if (fontStyle & FontStyle.Italic) { + r += 'italic '; + } + if (fontStyle & FontStyle.Bold) { + r += 'bold '; + } + if (fontStyle & FontStyle.Underline) { + r += 'underline '; + } + if (r.length === 0) { + r = '---'; + } + return r; + } + + private _getTokensAtPosition(grammar: IGrammar, position: Position): ITextMateTokenInfo { + const lineNumber = position.lineNumber; + let stateBeforeLine = this._getStateBeforeLine(grammar, lineNumber); + + let tokenizationResult1 = grammar.tokenizeLine(this._model.getLineContent(lineNumber), stateBeforeLine); + let tokenizationResult2 = grammar.tokenizeLine2(this._model.getLineContent(lineNumber), stateBeforeLine); + + let token1Index = 0; + for (let i = tokenizationResult1.tokens.length - 1; i >= 0; i--) { + let t = tokenizationResult1.tokens[i]; + if (position.column - 1 >= t.startIndex) { + token1Index = i; + break; + } + } + + let token2Index = 0; + for (let i = (tokenizationResult2.tokens.length >>> 1); i >= 0; i--) { + if (position.column - 1 >= tokenizationResult2.tokens[(i << 1)]) { + token2Index = i; + break; + } + } + + return { + token: tokenizationResult1.tokens[token1Index], + metadata: this._decodeMetadata(tokenizationResult2.tokens[(token2Index << 1) + 1]) + }; + } + + private _getStateBeforeLine(grammar: IGrammar, lineNumber: number): StackElement | null { + let state: StackElement | null = null; + + for (let i = 1; i < lineNumber; i++) { + let tokenizationResult = grammar.tokenizeLine(this._model.getLineContent(i), state); + state = tokenizationResult.ruleStack; + } + + return state; + } + + private isSemanticTokens(token: any): token is SemanticTokens { + return token && token.data; + } + + private async _computeSemanticTokens(): Promise { + const tokenProviders = DocumentSemanticTokensProviderRegistry.ordered(this._model); + if (tokenProviders.length) { + const provider = tokenProviders[0]; + const tokens = await Promise.resolve(provider.provideDocumentSemanticTokens(this._model, null, this._currentRequestCancellationTokenSource.token)); + if (this.isSemanticTokens(tokens)) { + return { tokens, legend: provider.getLegend() }; + } + } + return null; + } + + private _getSemanticTokenAtPosition(semanticTokens: SemanticTokensResult, pos: Position): ISemanticTokenInfo | null { + const tokenData = semanticTokens.tokens.data; + let lastLine = 0; + let lastCharacter = 0; + const posLine = pos.lineNumber - 1, posCharacter = pos.column - 1; // to 0-based position + for (let i = 0; i < tokenData.length; i += 5) { + const lineDelta = tokenData[i], charDelta = tokenData[i + 1], len = tokenData[i + 2], typeIdx = tokenData[i + 3], modSet = tokenData[i + 4]; + const line = lastLine + lineDelta; // 0-based + const character = lineDelta === 0 ? lastCharacter + charDelta : charDelta; // 0-based + if (posLine === line && character <= posCharacter && posCharacter < character + len) { + const type = semanticTokens.legend.tokenTypes[typeIdx]; + const modifiers = semanticTokens.legend.tokenModifiers.filter((_, k) => modSet & 1 << k); + const range = new Range(line + 1, character + 1, line + 1, character + 1 + len); + const metadata = this._decodeMetadata(this._themeService.getTheme().getTokenStyleMetadata(type, modifiers) || 0); + return { type, modifiers, range, metadata }; + } + lastLine = line; + lastCharacter = character; + } + return null; + } + + public getDomNode(): HTMLElement { + return this._domNode; + } + + public getPosition(): IContentWidgetPosition { + return { + position: this._editor.getPosition(), + preference: [ContentWidgetPositionPreference.BELOW, ContentWidgetPositionPreference.ABOVE] + }; + } +} + +registerEditorContribution(InspectEditorTokensController.ID, InspectEditorTokensController); +registerEditorAction(InspectEditorTokens); + +registerThemingParticipant((theme, collector) => { + const border = theme.getColor(editorHoverBorder); + if (border) { + let borderWidth = theme.type === HIGH_CONTRAST ? 2 : 1; + collector.addRule(`.monaco-editor .token-inspect-widget { border: ${borderWidth}px solid ${border}; }`); + collector.addRule(`.monaco-editor .token-inspect-widget .tiw-metadata-separator { background-color: ${border}; }`); + } + const background = theme.getColor(editorHoverBackground); + if (background) { + collector.addRule(`.monaco-editor .token-inspect-widget { background-color: ${background}; }`); + } +}); diff --git a/src/vs/workbench/contrib/codeEditor/browser/inspectTMScopes/inspectTMScopes.ts b/src/vs/workbench/contrib/codeEditor/browser/inspectTMScopes/inspectTMScopes.ts deleted file mode 100644 index 4b05fdd1a6..0000000000 --- a/src/vs/workbench/contrib/codeEditor/browser/inspectTMScopes/inspectTMScopes.ts +++ /dev/null @@ -1,392 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import 'vs/css!./inspectTMScopes'; -import * as nls from 'vs/nls'; -import * as dom from 'vs/base/browser/dom'; -import { CharCode } from 'vs/base/common/charCode'; -import { Color } from 'vs/base/common/color'; -import { KeyCode } from 'vs/base/common/keyCodes'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { escape } from 'vs/base/common/strings'; -import { ContentWidgetPositionPreference, IActiveCodeEditor, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; -import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { Position } from 'vs/editor/common/core/position'; -import { IEditorContribution } from 'vs/editor/common/editorCommon'; -import { ITextModel } from 'vs/editor/common/model'; -import { FontStyle, LanguageIdentifier, StandardTokenType, TokenMetadata, TokenizationRegistry } from 'vs/editor/common/modes'; -import { IModeService } from 'vs/editor/common/services/modeService'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { editorHoverBackground, editorHoverBorder } from 'vs/platform/theme/common/colorRegistry'; -import { HIGH_CONTRAST, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { findMatchingThemeRule } from 'vs/workbench/services/textMate/common/TMHelper'; -import { ITextMateService, IGrammar, IToken, StackElement } from 'vs/workbench/services/textMate/common/textMateService'; -import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; - -class InspectTMScopesController extends Disposable implements IEditorContribution { - - public static readonly ID = 'editor.contrib.inspectTMScopes'; - - public static get(editor: ICodeEditor): InspectTMScopesController { - return editor.getContribution(InspectTMScopesController.ID); - } - - private _editor: ICodeEditor; - private _textMateService: ITextMateService; - private _themeService: IWorkbenchThemeService; - private _modeService: IModeService; - private _notificationService: INotificationService; - private _widget: InspectTMScopesWidget | null; - - constructor( - editor: ICodeEditor, - @ITextMateService textMateService: ITextMateService, - @IModeService modeService: IModeService, - @IWorkbenchThemeService themeService: IWorkbenchThemeService, - @INotificationService notificationService: INotificationService - ) { - super(); - this._editor = editor; - this._textMateService = textMateService; - this._themeService = themeService; - this._modeService = modeService; - this._notificationService = notificationService; - this._widget = null; - - this._register(this._editor.onDidChangeModel((e) => this.stop())); - this._register(this._editor.onDidChangeModelLanguage((e) => this.stop())); - this._register(this._editor.onKeyUp((e) => e.keyCode === KeyCode.Escape && this.stop())); - } - - public dispose(): void { - this.stop(); - super.dispose(); - } - - public launch(): void { - if (this._widget) { - return; - } - if (!this._editor.hasModel()) { - return; - } - this._widget = new InspectTMScopesWidget(this._editor, this._textMateService, this._modeService, this._themeService, this._notificationService); - } - - public stop(): void { - if (this._widget) { - this._widget.dispose(); - this._widget = null; - } - } - - public toggle(): void { - if (!this._widget) { - this.launch(); - } else { - this.stop(); - } - } -} - -class InspectTMScopes extends EditorAction { - - constructor() { - super({ - id: 'editor.action.inspectTMScopes', - label: nls.localize('inspectTMScopes', "Developer: Inspect TM Scopes"), - alias: 'Developer: Inspect TM Scopes', - precondition: undefined - }); - } - - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { - let controller = InspectTMScopesController.get(editor); - if (controller) { - controller.toggle(); - } - } -} - -interface ICompleteLineTokenization { - startState: StackElement | null; - tokens1: IToken[]; - tokens2: Uint32Array; - endState: StackElement; -} - -interface IDecodedMetadata { - languageIdentifier: LanguageIdentifier; - tokenType: StandardTokenType; - fontStyle: FontStyle; - foreground: Color; - background: Color; -} - -function renderTokenText(tokenText: string): string { - if (tokenText.length > 40) { - tokenText = tokenText.substr(0, 20) + '…' + tokenText.substr(tokenText.length - 20); - } - let result: string = ''; - for (let charIndex = 0, len = tokenText.length; charIndex < len; charIndex++) { - let charCode = tokenText.charCodeAt(charIndex); - switch (charCode) { - case CharCode.Tab: - result += '→'; - break; - - case CharCode.Space: - result += '·'; - break; - - case CharCode.LessThan: - result += '<'; - break; - - case CharCode.GreaterThan: - result += '>'; - break; - - case CharCode.Ampersand: - result += '&'; - break; - - default: - result += String.fromCharCode(charCode); - } - } - return result; -} - -class InspectTMScopesWidget extends Disposable implements IContentWidget { - - private static readonly _ID = 'editor.contrib.inspectTMScopesWidget'; - - // Editor.IContentWidget.allowEditorOverflow - public readonly allowEditorOverflow = true; - - private _isDisposed: boolean; - private readonly _editor: IActiveCodeEditor; - private readonly _modeService: IModeService; - private readonly _themeService: IWorkbenchThemeService; - private readonly _notificationService: INotificationService; - private readonly _model: ITextModel; - private readonly _domNode: HTMLElement; - private readonly _grammar: Promise; - - constructor( - editor: IActiveCodeEditor, - textMateService: ITextMateService, - modeService: IModeService, - themeService: IWorkbenchThemeService, - notificationService: INotificationService - ) { - super(); - this._isDisposed = false; - this._editor = editor; - this._modeService = modeService; - this._themeService = themeService; - this._notificationService = notificationService; - this._model = this._editor.getModel(); - this._domNode = document.createElement('div'); - this._domNode.className = 'tm-inspect-widget'; - this._grammar = textMateService.createGrammar(this._model.getLanguageIdentifier().language); - this._beginCompute(this._editor.getPosition()); - this._register(this._editor.onDidChangeCursorPosition((e) => this._beginCompute(this._editor.getPosition()))); - this._editor.addContentWidget(this); - } - - public dispose(): void { - this._isDisposed = true; - this._editor.removeContentWidget(this); - super.dispose(); - } - - public getId(): string { - return InspectTMScopesWidget._ID; - } - - private _beginCompute(position: Position): void { - dom.clearNode(this._domNode); - this._domNode.appendChild(document.createTextNode(nls.localize('inspectTMScopesWidget.loading', "Loading..."))); - this._grammar.then( - (grammar) => { - if (!grammar) { - throw new Error(`Could not find grammar for language!`); - } - this._compute(grammar, position); - }, - (err) => { - this._notificationService.warn(err); - setTimeout(() => { - InspectTMScopesController.get(this._editor).stop(); - }); - } - ); - } - - private _compute(grammar: IGrammar, position: Position): void { - if (this._isDisposed) { - return; - } - let data = this._getTokensAtLine(grammar, position.lineNumber); - - let token1Index = 0; - for (let i = data.tokens1.length - 1; i >= 0; i--) { - let t = data.tokens1[i]; - if (position.column - 1 >= t.startIndex) { - token1Index = i; - break; - } - } - - let token2Index = 0; - for (let i = (data.tokens2.length >>> 1); i >= 0; i--) { - if (position.column - 1 >= data.tokens2[(i << 1)]) { - token2Index = i; - break; - } - } - - let result = ''; - - let tokenStartIndex = data.tokens1[token1Index].startIndex; - let tokenEndIndex = data.tokens1[token1Index].endIndex; - let tokenText = this._model.getLineContent(position.lineNumber).substring(tokenStartIndex, tokenEndIndex); - result += `

${renderTokenText(tokenText)}(${tokenText.length} ${tokenText.length === 1 ? 'char' : 'chars'})

`; - - result += ``; - - let metadata = this._decodeMetadata(data.tokens2[(token2Index << 1) + 1]); - result += ``; - result += ``; - result += ``; - result += ``; - result += ``; - result += ``; - if (metadata.background.isOpaque()) { - result += ``; - } else { - result += ''; - } - result += ``; - - let theme = this._themeService.getColorTheme(); - result += ``; - let matchingRule = findMatchingThemeRule(theme, data.tokens1[token1Index].scopes, false); - if (matchingRule) { - result += `${matchingRule.rawSelector}\n${JSON.stringify(matchingRule.settings, null, '\t')}`; - } else { - result += `No theme selector.`; - } - - result += ``; - - result += `
    `; - for (let i = data.tokens1[token1Index].scopes.length - 1; i >= 0; i--) { - result += `
  • ${escape(data.tokens1[token1Index].scopes[i])}
  • `; - } - result += `
`; - - - this._domNode.innerHTML = result; - this._editor.layoutContentWidget(this); - } - - private _decodeMetadata(metadata: number): IDecodedMetadata { - let colorMap = TokenizationRegistry.getColorMap()!; - let languageId = TokenMetadata.getLanguageId(metadata); - let tokenType = TokenMetadata.getTokenType(metadata); - let fontStyle = TokenMetadata.getFontStyle(metadata); - let foreground = TokenMetadata.getForeground(metadata); - let background = TokenMetadata.getBackground(metadata); - return { - languageIdentifier: this._modeService.getLanguageIdentifier(languageId)!, - tokenType: tokenType, - fontStyle: fontStyle, - foreground: colorMap[foreground], - background: colorMap[background] - }; - } - - private _tokenTypeToString(tokenType: StandardTokenType): string { - switch (tokenType) { - case StandardTokenType.Other: return 'Other'; - case StandardTokenType.Comment: return 'Comment'; - case StandardTokenType.String: return 'String'; - case StandardTokenType.RegEx: return 'RegEx'; - } - return '??'; - } - - private _fontStyleToString(fontStyle: FontStyle): string { - let r = ''; - if (fontStyle & FontStyle.Italic) { - r += 'italic '; - } - if (fontStyle & FontStyle.Bold) { - r += 'bold '; - } - if (fontStyle & FontStyle.Underline) { - r += 'underline '; - } - if (r.length === 0) { - r = '---'; - } - return r; - } - - private _getTokensAtLine(grammar: IGrammar, lineNumber: number): ICompleteLineTokenization { - let stateBeforeLine = this._getStateBeforeLine(grammar, lineNumber); - - let tokenizationResult1 = grammar.tokenizeLine(this._model.getLineContent(lineNumber), stateBeforeLine); - let tokenizationResult2 = grammar.tokenizeLine2(this._model.getLineContent(lineNumber), stateBeforeLine); - - return { - startState: stateBeforeLine, - tokens1: tokenizationResult1.tokens, - tokens2: tokenizationResult2.tokens, - endState: tokenizationResult1.ruleStack - }; - } - - private _getStateBeforeLine(grammar: IGrammar, lineNumber: number): StackElement | null { - let state: StackElement | null = null; - - for (let i = 1; i < lineNumber; i++) { - let tokenizationResult = grammar.tokenizeLine(this._model.getLineContent(i), state); - state = tokenizationResult.ruleStack; - } - - return state; - } - - public getDomNode(): HTMLElement { - return this._domNode; - } - - public getPosition(): IContentWidgetPosition { - return { - position: this._editor.getPosition(), - preference: [ContentWidgetPositionPreference.BELOW, ContentWidgetPositionPreference.ABOVE] - }; - } -} - -registerEditorContribution(InspectTMScopesController.ID, InspectTMScopesController); -registerEditorAction(InspectTMScopes); - -registerThemingParticipant((theme, collector) => { - const border = theme.getColor(editorHoverBorder); - if (border) { - let borderWidth = theme.type === HIGH_CONTRAST ? 2 : 1; - collector.addRule(`.monaco-editor .tm-inspect-widget { border: ${borderWidth}px solid ${border}; }`); - collector.addRule(`.monaco-editor .tm-inspect-widget .tm-metadata-separator { background-color: ${border}; }`); - } - const background = theme.getColor(editorHoverBackground); - if (background) { - collector.addRule(`.monaco-editor .tm-inspect-widget { background-color: ${background}; }`); - } -}); diff --git a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts index 37cdf82fb7..10af5fe474 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts @@ -226,8 +226,8 @@ export class SuggestEnabledInput extends Widget implements IThemable { public style(colors: ISuggestEnabledInputStyles): void { this.placeholderText.style.backgroundColor = this.stylingContainer.style.backgroundColor = colors.inputBackground ? colors.inputBackground.toString() : ''; - this.stylingContainer.style.color = colors.inputForeground ? colors.inputForeground.toString() : null; - this.placeholderText.style.color = colors.inputPlaceholderForeground ? colors.inputPlaceholderForeground.toString() : null; + this.stylingContainer.style.color = colors.inputForeground ? colors.inputForeground.toString() : ''; + this.placeholderText.style.color = colors.inputPlaceholderForeground ? colors.inputPlaceholderForeground.toString() : ''; this.stylingContainer.style.borderWidth = '1px'; this.stylingContainer.style.borderStyle = 'solid'; diff --git a/src/vs/workbench/contrib/codeEditor/electron-browser/selectionClipboard.ts b/src/vs/workbench/contrib/codeEditor/electron-browser/selectionClipboard.ts index 3b56e368b1..63194dfe67 100644 --- a/src/vs/workbench/contrib/codeEditor/electron-browser/selectionClipboard.ts +++ b/src/vs/workbench/contrib/codeEditor/electron-browser/selectionClipboard.ts @@ -3,15 +3,16 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as nls from 'vs/nls'; import { RunOnceScheduler } from 'vs/base/common/async'; import { Disposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { registerEditorContribution, EditorAction, ServicesAccessor, registerEditorAction } from 'vs/editor/browser/editorExtensions'; import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; import { ICursorSelectionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; import { Range } from 'vs/editor/common/core/range'; -import { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { IEditorContribution, Handler } from 'vs/editor/common/editorCommon'; import { EndOfLinePreference } from 'vs/editor/common/model'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; @@ -19,6 +20,10 @@ 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; @@ -107,5 +112,41 @@ class SelectionClipboardPastePreventer implements IWorkbenchContribution { } } +class PasteSelectionClipboardAction extends EditorAction { + + constructor() { + super({ + 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 + } + }); + } + + public async run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): Promise { + const clipboardService = accessor.get(IClipboardService); + + // read selection clipboard + const text = await clipboardService.readText('selection'); + + editor.trigger('keyboard', Handler.Paste, { + text: text, + pasteOnNewLine: false, + multicursorText: null + }); + } +} + registerEditorContribution(SelectionClipboardContributionID, SelectionClipboard); Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(SelectionClipboardPastePreventer, LifecyclePhase.Ready); +if (platform.isLinux) { + registerEditorAction(PasteSelectionClipboardAction); +} diff --git a/src/vs/workbench/contrib/customEditor/browser/commands.ts b/src/vs/workbench/contrib/customEditor/browser/commands.ts index 20981ac84c..62c565bb03 100644 --- a/src/vs/workbench/contrib/customEditor/browser/commands.ts +++ b/src/vs/workbench/contrib/customEditor/browser/commands.ts @@ -3,59 +3,38 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { firstOrDefault } from 'vs/base/common/arrays'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { URI } from 'vs/base/common/uri'; import { Command } from 'vs/editor/browser/editorExtensions'; -import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import * as nls from 'vs/nls'; import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { IListService } from 'vs/platform/list/browser/listService'; +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 { 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 { getMultiSelectedResources } from 'vs/workbench/contrib/files/browser/files'; -import { IExplorerService } from 'vs/workbench/contrib/files/common/files'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { CustomFileEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput'; +import type { ITextEditorOptions } from 'vs/platform/editor/common/editor'; const viewCategory = nls.localize('viewCategory', "View"); // #region Open With -const OPEN_WITH_COMMAND_ID = 'openWith'; -// const OPEN_WITH_TITLE = { value: nls.localize('openWith.title', 'Open With'), original: 'Open With' }; +CommandsRegistry.registerCommand('_workbench.openWith', (accessor: ServicesAccessor, args: [URI, string, ITextEditorOptions | undefined, EditorViewColumn | undefined]) => { + const customEditorService = accessor.get(ICustomEditorService); + const editorGroupService = accessor.get(IEditorGroupsService); -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: OPEN_WITH_COMMAND_ID, - weight: KeybindingWeight.WorkbenchContrib, - when: EditorContextKeys.focus.toNegated(), - handler: async (accessor: ServicesAccessor, resource: URI | object) => { - const editorService = accessor.get(IEditorService); - const resources = getMultiSelectedResources(resource, accessor.get(IListService), editorService, accessor.get(IExplorerService)); - const targetResource = firstOrDefault(resources); - if (!targetResource) { - return undefined; // {{SQL CARBON EDIT}} strict-null-check - } - return accessor.get(ICustomEditorService).promptOpenWith(targetResource, undefined, undefined); - } + const [resource, viewType, options, position] = args; + const group = viewColumnToEditorGroup(editorGroupService, position); + customEditorService.openWith(resource, viewType, options, editorGroupService.getGroup(group)); }); -// MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { -// group: 'navigation', -// order: 20, -// command: { -// id: OPEN_WITH_COMMAND_ID, -// title: OPEN_WITH_TITLE, -// }, -// when: ResourceContextKey.Scheme.isEqualTo(Schemas.file) -// }); - // #endregion // #region Reopen With diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index 9c48da32c9..c964d8f816 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -21,6 +21,7 @@ import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileE import { WebviewEditorOverlay } from 'vs/workbench/contrib/webview/browser/webview'; import { IWebviewWorkbenchService, LazilyResolvedWebviewEditorInput } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { @@ -41,6 +42,7 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { @ICustomEditorService private readonly customEditorService: ICustomEditorService, @IEditorService private readonly editorService: IEditorService, @IFileDialogService private readonly fileDialogService: IFileDialogService, + @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService ) { super(id, viewType, '', webview, webviewWorkbenchService, lifecycleService); this._editorResource = resource; @@ -109,6 +111,18 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { return this._model ? this._model.isDirty() : false; } + public isSaving(): boolean { + if (!this.isDirty()) { + return false; // the editor needs to be dirty for being saved + } + + if (this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) { + return true; // a short auto save is configured, treat this as being saved + } + + return false; + } + public save(groupId: GroupIdentifier, options?: ISaveOptions): Promise { return this._model ? this._model.save(options) : Promise.resolve(false); } @@ -141,7 +155,7 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { } public async resolve(): Promise { - this._model = await this.customEditorService.models.loadOrCreate(this.getResource(), this.viewType); + this._model = await this.customEditorService.models.resolve(this.getResource(), this.viewType); this._register(this._model.onDidChangeDirty(() => this._onDidChangeDirty.fire())); if (this.isDirty()) { this._onDidChangeDirty.fire(); @@ -149,15 +163,12 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { return await super.resolve(); } - protected async promptForPath(resource: URI, defaultUri: URI, availableFileSystems?: readonly string[]): Promise { + private async promptForPath(resource: URI, defaultUri: URI, availableFileSystems?: string[]): Promise { // Help user to find a name for the file by opening it first await this.editorService.openEditor({ resource, options: { revealIfOpened: true, preserveFocus: true } }); - return this.fileDialogService.pickFileToSave({ - availableFileSystems, - defaultUri - }); + return this.fileDialogService.pickFileToSave(defaultUri, availableFileSystems); } public handleMove(groupId: GroupIdentifier, uri: URI, options?: ITextEditorOptions): IEditorInput | undefined { diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts index 3db48bc88d..1c8e9c7aa4 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts @@ -13,6 +13,7 @@ import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { FileOperation, IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import * as colorRegistry from 'vs/platform/theme/common/colorRegistry'; @@ -25,10 +26,11 @@ import { CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, CONTEXT_HAS_CUSTOM_EDITORS, import { CustomEditorModelManager } from 'vs/workbench/contrib/customEditor/common/customEditorModelManager'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { IWebviewService, webviewHasOwnEditFunctionsContext } from 'vs/workbench/contrib/webview/browser/webview'; -import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +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 { Emitter } from 'vs/base/common/event'; export const defaultEditorId = 'default'; const defaultEditorInfo = new CustomEditorInfo({ @@ -40,37 +42,57 @@ const defaultEditorInfo = new CustomEditorInfo({ priority: CustomEditorPriority.default, }); -export class CustomEditorInfoStore { - private readonly contributedEditors = new Map(); +export class CustomEditorInfoStore extends Disposable { - public clear() { - this.contributedEditors.clear(); + private readonly _contributedEditors = new Map(); + + constructor() { + super(); + + webviewEditorsExtensionPoint.setHandler(extensions => { + this._contributedEditors.clear(); + + for (const extension of extensions) { + for (const webviewEditorContribution of extension.value) { + this.add(new CustomEditorInfo({ + id: webviewEditorContribution.viewType, + displayName: webviewEditorContribution.displayName, + selector: webviewEditorContribution.selector || [], + priority: webviewEditorContribution.priority || CustomEditorPriority.default, + })); + } + } + this._onChange.fire(); + }); } + private readonly _onChange = this._register(new Emitter()); + public readonly onChange = this._onChange.event; + public get(viewType: string): CustomEditorInfo | undefined { return viewType === defaultEditorId ? defaultEditorInfo - : this.contributedEditors.get(viewType); - } - - public add(info: CustomEditorInfo): void { - if (info.id === defaultEditorId || this.contributedEditors.has(info.id)) { - console.log(`Custom editor with id '${info.id}' already registered`); - return; - } - this.contributedEditors.set(info.id, info); + : this._contributedEditors.get(viewType); } public getContributedEditors(resource: URI): readonly CustomEditorInfo[] { - return Array.from(this.contributedEditors.values()).filter(customEditor => - customEditor.matches(resource)); + return Array.from(this._contributedEditors.values()) + .filter(customEditor => customEditor.matches(resource)); + } + + private add(info: CustomEditorInfo): void { + if (info.id === defaultEditorId || this._contributedEditors.has(info.id)) { + console.error(`Custom editor with id '${info.id}' already registered`); + return; + } + this._contributedEditors.set(info.id, info); } } export class CustomEditorService extends Disposable implements ICustomEditorService { _serviceBrand: any; - private readonly _editorInfoStore = new CustomEditorInfoStore(); + private readonly _editorInfoStore = this._register(new CustomEditorInfoStore()); private readonly _models: CustomEditorModelManager; @@ -81,8 +103,10 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ constructor( @IContextKeyService contextKeyService: IContextKeyService, @IWorkingCopyService workingCopyService: IWorkingCopyService, + @IFileService fileService: IFileService, @IConfigurationService private readonly configurationService: IConfigurationService, @IEditorService private readonly editorService: IEditorService, + @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IQuickInputService private readonly quickInputService: IQuickInputService, @IWebviewService private readonly webviewService: IWebviewService, @@ -91,27 +115,19 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ this._models = new CustomEditorModelManager(workingCopyService); - webviewEditorsExtensionPoint.setHandler(extensions => { - this._editorInfoStore.clear(); - - for (const extension of extensions) { - for (const webviewEditorContribution of extension.value) { - this._editorInfoStore.add(new CustomEditorInfo({ - id: webviewEditorContribution.viewType, - displayName: webviewEditorContribution.displayName, - selector: webviewEditorContribution.selector || [], - priority: webviewEditorContribution.priority || CustomEditorPriority.default, - })); - } - } - this.updateContexts(); - }); - this._hasCustomEditor = CONTEXT_HAS_CUSTOM_EDITORS.bindTo(contextKeyService); this._focusedCustomEditorIsEditable = CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE.bindTo(contextKeyService); this._webviewHasOwnEditFunctions = webviewHasOwnEditFunctionsContext.bindTo(contextKeyService); + this._register(this._editorInfoStore.onChange(() => this.updateContexts())); this._register(this.editorService.onDidActiveEditorChange(() => this.updateContexts())); + + this._register(fileService.onAfterOperation(e => { + if (e.isOperation(FileOperation.MOVE)) { + this.handleMovedFileInOpenedFileEditors(e.resource, e.target.resource); + } + })); + this.updateContexts(); } @@ -224,23 +240,25 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ options?: IEditorOptions, group?: IEditorGroup ): Promise { - if (group) { - const existingEditors = group.editors.filter(editor => editor.getResource() && isEqual(editor.getResource(), resource)); - if (existingEditors.length) { - const existing = existingEditors[0]; - if (!input.matches(existing)) { - await this.editorService.replaceEditors([{ - editor: existing, - replacement: input, - options: options ? EditorOptions.create(options) : undefined, - }], group); + const targetGroup = group || this.editorGroupService.activeGroup; - if (existing instanceof CustomFileEditorInput) { - existing.dispose(); - } + // Try to replace existing editors for resource + const existingEditors = targetGroup.editors.filter(editor => editor.getResource() && isEqual(editor.getResource(), resource)); + if (existingEditors.length) { + const existing = existingEditors[0]; + if (!input.matches(existing)) { + await this.editorService.replaceEditors([{ + editor: existing, + replacement: input, + options: options ? EditorOptions.create(options) : undefined, + }], targetGroup); + + if (existing instanceof CustomFileEditorInput) { + existing.dispose(); } } } + return this.editorService.openEditor(input, options, group); } @@ -262,6 +280,31 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ this._focusedCustomEditorIsEditable.set(activeControl?.input instanceof CustomFileEditorInput); this._webviewHasOwnEditFunctions.set(true); } + + private handleMovedFileInOpenedFileEditors(oldResource: URI, newResource: URI): void { + for (const group of this.editorGroupService.groups) { + for (const editor of group.editors) { + if (!(editor instanceof CustomFileEditorInput)) { + continue; + } + + const editorInfo = this._editorInfoStore.get(editor.viewType); + if (!editorInfo) { + continue; + } + + if (!editorInfo.matches(newResource)) { + continue; + } + + const replacement = this.createInput(newResource, editor.viewType, group); + this.editorService.replaceEditors([{ + editor: editor, + replacement: replacement, + }], group); + } + } + } } export const customEditorsAssociationsKey = 'workbench.experimental.editorAssociations'; @@ -305,13 +348,28 @@ export class CustomEditorContribution implements IWorkbenchContribution { group: IEditorGroup ): IOpenEditorOverride | undefined { const userConfiguredEditors = this.customEditorService.getUserConfiguredCustomEditors(resource); + const contributedEditors = this.customEditorService.getContributedCustomEditors(resource); + if (!userConfiguredEditors.length && !contributedEditors.length) { + return undefined; // {{SQL CARBON EDIT}} Strict-null-checks + } + + // Check to see if there already an editor for the resource in the group. + // If there is, we want to open that instead of creating a new editor. + // This ensures that we preserve whatever type of editor was previously being used + // when the user switches back to it. + const existingEditorForResource = group.editors.find(editor => isEqual(resource, editor.getResource())); + if (existingEditorForResource) { + return { + override: this.editorService.openEditor(existingEditorForResource, { ...options, ignoreOverrides: true }, group) + }; + } + if (userConfiguredEditors.length) { return { override: this.customEditorService.openWith(resource, userConfiguredEditors.allEditors[0].id, options, group), }; } - const contributedEditors = this.customEditorService.getContributedCustomEditors(resource); if (!contributedEditors.length) { return undefined; // {{SQL CARBON EDIT}} Strict-null-checks } diff --git a/src/vs/workbench/contrib/customEditor/common/customEditor.ts b/src/vs/workbench/contrib/customEditor/common/customEditor.ts index 4b9b7a4c34..400a85d4e5 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditor.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditor.ts @@ -47,7 +47,7 @@ export type CustomEditorEdit = { source?: any, data: any }; export interface ICustomEditorModelManager { get(resource: URI, viewType: string): ICustomEditorModel | undefined; - loadOrCreate(resource: URI, viewType: string): Promise; + resolve(resource: URI, viewType: string): Promise; disposeModel(model: ICustomEditorModel): void; } diff --git a/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts b/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts index a6a8256411..ab811f7687 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts @@ -39,6 +39,9 @@ export class CustomEditorModel extends Disposable implements ICustomEditorModel protected readonly _onDidChangeDirty: Emitter = this._register(new Emitter()); readonly onDidChangeDirty: Event = this._onDidChangeDirty.event; + protected readonly _onDidChangeContent: Emitter = this._register(new Emitter()); + readonly onDidChangeContent: Event = this._onDidChangeContent.event; + //#endregion protected readonly _onUndo = this._register(new Emitter()); @@ -62,12 +65,21 @@ export class CustomEditorModel extends Disposable implements ICustomEditorModel this._currentEditIndex = this._edits.length - 1; this.updateDirty(); this._onApplyEdit.fire([edit]); + this.updateContentChanged(); } private updateDirty() { + // TODO@matt this should to be more fine grained and avoid + // emitting events if there was no change actually this._onDidChangeDirty.fire(); } + 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 async save(_options?: ISaveOptions): Promise { const untils: Promise[] = []; const handler: CustomEditorSaveEvent = { @@ -125,6 +137,7 @@ export class CustomEditorModel extends Disposable implements ICustomEditorModel this._currentEditIndex = this._savePoint; this._edits.splice(this._currentEditIndex + 1, this._edits.length - this._currentEditIndex); this.updateDirty(); + this.updateContentChanged(); return true; } @@ -139,6 +152,7 @@ export class CustomEditorModel extends Disposable implements ICustomEditorModel this._onUndo.fire([{ data: undoneEdit }]); this.updateDirty(); + this.updateContentChanged(); } public redo() { @@ -153,5 +167,14 @@ export class CustomEditorModel extends Disposable implements ICustomEditorModel this._onApplyEdit.fire([{ data: redoneEdit }]); this.updateDirty(); + this.updateContentChanged(); + } + + public hasBackup(): boolean { + return true; //TODO@matt forward to extension + } + + public async backup(): Promise { + //TODO@matt forward to extension } } diff --git a/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts b/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts index eb9630f8c3..3d7d0d1d80 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditorModelManager.ts @@ -21,7 +21,7 @@ export class CustomEditorModelManager implements ICustomEditorModelManager { return this._models.get(this.key(resource, viewType))?.model; } - public async loadOrCreate(resource: URI, viewType: string): Promise { + public async resolve(resource: URI, viewType: string): Promise { const existing = this.get(resource, viewType); if (existing) { return existing; @@ -29,7 +29,7 @@ export class CustomEditorModelManager implements ICustomEditorModelManager { const model = new CustomEditorModel(resource); const disposables = new DisposableStore(); - this._workingCopyService.registerWorkingCopy(model); + disposables.add(this._workingCopyService.registerWorkingCopy(model)); this._models.set(this.key(resource, viewType), { model, disposables }); return model; } diff --git a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts index b83c3d1167..2615f8233a 100644 --- a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts +++ b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts @@ -9,7 +9,7 @@ import { Expression, Variable, ExpressionContainer } from 'vs/workbench/contrib/ import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInputValidationOptions, InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { ITreeRenderer, ITreeNode } from 'vs/base/browser/ui/tree/tree'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; import { KeyCode } from 'vs/base/common/keyCodes'; @@ -18,6 +18,7 @@ import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabe import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; import { ReplEvaluationResult } from 'vs/workbench/contrib/debug/common/replModel'; +import { once } from 'vs/base/common/functional'; export const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024; export const twistiePixels = 20; @@ -42,7 +43,7 @@ export interface IVariableTemplateData { } export function renderViewTree(container: HTMLElement): HTMLElement { - const treeContainer = document.createElement('div'); + const treeContainer = $('.'); dom.addClass(treeContainer, 'debug-view-content'); container.appendChild(treeContainer); return treeContainer; @@ -137,8 +138,7 @@ export interface IExpressionTemplateData { name: HTMLSpanElement; value: HTMLSpanElement; inputBoxContainer: HTMLElement; - enableInputBox(options: IInputBoxOptions): void; - toDispose: IDisposable[]; + toDispose: IDisposable; label: HighlightedLabel; } @@ -159,77 +159,84 @@ export abstract class AbstractExpressionsRenderer implements ITreeRenderer { - name.style.display = 'none'; - value.style.display = 'none'; - inputBoxContainer.style.display = 'initial'; - - const inputBox = new InputBox(inputBoxContainer, this.contextViewService, options); - const styler = attachInputBoxStyler(inputBox, this.themeService); - - inputBox.value = replaceWhitespace(options.initialValue); - inputBox.focus(); - inputBox.select(); - - let disposed = false; - toDispose.push(inputBox); - toDispose.push(styler); - - const wrapUp = (renamed: boolean) => { - if (!disposed) { - disposed = true; - this.debugService.getViewModel().setSelectedExpression(undefined); - options.onFinish(inputBox.value, renamed); - - // need to remove the input box since this template will be reused. - inputBoxContainer.removeChild(inputBox.element); - name.style.display = 'initial'; - value.style.display = 'initial'; - inputBoxContainer.style.display = 'none'; - dispose(toDispose); - } - }; - - toDispose.push(dom.addStandardDisposableListener(inputBox.inputElement, 'keydown', (e: IKeyboardEvent) => { - const isEscape = e.equals(KeyCode.Escape); - const isEnter = e.equals(KeyCode.Enter); - if (isEscape || isEnter) { - e.preventDefault(); - e.stopPropagation(); - wrapUp(isEnter); - } - })); - toDispose.push(dom.addDisposableListener(inputBox.inputElement, 'blur', () => { - wrapUp(true); - })); - toDispose.push(dom.addDisposableListener(inputBox.inputElement, 'click', e => { - // Do not expand / collapse selected elements - e.preventDefault(); - e.stopPropagation(); - })); - }; - - return { expression, name, value, label, enableInputBox, inputBoxContainer, toDispose }; + return { expression, name, value, label, inputBoxContainer, toDispose: Disposable.None }; } renderElement(node: ITreeNode, index: number, data: IExpressionTemplateData): void { + data.toDispose.dispose(); + data.toDispose = Disposable.None; const { element } = node; if (element === this.debugService.getViewModel().getSelectedExpression() || (element instanceof Variable && element.errorMessage)) { const options = this.getInputBoxOptions(element); if (options) { - data.enableInputBox(options); + data.toDispose = this.renderInputBox(data.name, data.value, data.inputBoxContainer, options); return; } } this.renderExpression(element, data, createMatches(node.filterData)); } + renderInputBox(nameElement: HTMLElement, valueElement: HTMLElement, inputBoxContainer: HTMLElement, options: IInputBoxOptions): IDisposable { + nameElement.style.display = 'none'; + valueElement.style.display = 'none'; + inputBoxContainer.style.display = 'initial'; + + const inputBox = new InputBox(inputBoxContainer, this.contextViewService, options); + const styler = attachInputBoxStyler(inputBox, this.themeService); + + inputBox.value = replaceWhitespace(options.initialValue); + inputBox.focus(); + inputBox.select(); + + const done = once((success: boolean, finishEditing: boolean) => { + nameElement.style.display = 'initial'; + valueElement.style.display = 'initial'; + inputBoxContainer.style.display = 'none'; + const value = inputBox.value; + dispose(toDispose); + + if (finishEditing) { + this.debugService.getViewModel().setSelectedExpression(undefined); + options.onFinish(value, success); + } + }); + + const toDispose = [ + inputBox, + dom.addStandardDisposableListener(inputBox.inputElement, dom.EventType.KEY_DOWN, (e: IKeyboardEvent) => { + const isEscape = e.equals(KeyCode.Escape); + const isEnter = e.equals(KeyCode.Enter); + if (isEscape || isEnter) { + e.preventDefault(); + e.stopPropagation(); + done(isEnter, true); + } + }), + dom.addDisposableListener(inputBox.inputElement, dom.EventType.BLUR, () => { + done(true, true); + }), + dom.addDisposableListener(inputBox.inputElement, dom.EventType.CLICK, e => { + // Do not expand / collapse selected elements + e.preventDefault(); + e.stopPropagation(); + }), + styler + ]; + + return toDisposable(() => { + done(false, false); + }); + } + protected abstract renderExpression(expression: IExpression, data: IExpressionTemplateData, highlights: IHighlight[]): void; protected abstract getInputBoxOptions(expression: IExpression): IInputBoxOptions | undefined; + disposeElement(node: ITreeNode, index: number, templateData: IExpressionTemplateData): void { + templateData.toDispose.dispose(); + } + disposeTemplate(templateData: IExpressionTemplateData): void { - dispose(templateData.toDispose); + templateData.toDispose.dispose(); } } diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index 08e954e2f5..07aa11ac43 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -11,13 +11,12 @@ import severity from 'vs/base/common/severity'; import { IAction, Action } from 'vs/base/common/actions'; import { Range } from 'vs/editor/common/core/range'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType, IContentWidget, IActiveCodeEditor, IContentWidgetPosition, ContentWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; -import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { IModelDecorationOptions, IModelDeltaDecoration, TrackedRangeStickiness, ITextModel, OverviewRulerLane, IModelDecorationOverviewRulerOptions } from 'vs/editor/common/model'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { RemoveBreakpointAction } from 'vs/workbench/contrib/debug/browser/debugActions'; -import { IDebugService, IBreakpoint, CONTEXT_BREAKPOINT_WIDGET_VISIBLE, BreakpointWidgetContext, BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution, IBreakpointUpdateData, IDebugConfiguration } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, IBreakpoint, CONTEXT_BREAKPOINT_WIDGET_VISIBLE, BreakpointWidgetContext, IBreakpointEditorContribution, IBreakpointUpdateData, IDebugConfiguration, State, IDebugSession } from 'vs/workbench/contrib/debug/common/debug'; import { IMarginData } from 'vs/editor/browser/controller/mouseTarget'; import { ContextSubMenu } from 'vs/base/browser/contextmenu'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; @@ -51,7 +50,7 @@ const breakpointHelperDecoration: IModelDecorationOptions = { stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }; -function createBreakpointDecorations(model: ITextModel, breakpoints: ReadonlyArray, debugService: IDebugService, debugSettings: IDebugConfiguration): { range: Range; options: IModelDecorationOptions; }[] { +export function createBreakpointDecorations(model: ITextModel, breakpoints: ReadonlyArray, state: State, breakpointsActivated: boolean, showBreakpointsInOverviewRuler: boolean): { range: Range; options: IModelDecorationOptions; }[] { const result: { range: Range; options: IModelDecorationOptions; }[] = []; breakpoints.forEach((breakpoint) => { if (breakpoint.lineNumber > model.getLineCount()) { @@ -64,7 +63,7 @@ function createBreakpointDecorations(model: ITextModel, breakpoints: ReadonlyArr ); result.push({ - options: getBreakpointDecorationOptions(model, breakpoint, debugService, debugSettings), + options: getBreakpointDecorationOptions(model, breakpoint, state, breakpointsActivated, showBreakpointsInOverviewRuler), range }); }); @@ -72,8 +71,8 @@ function createBreakpointDecorations(model: ITextModel, breakpoints: ReadonlyArr return result; } -function getBreakpointDecorationOptions(model: ITextModel, breakpoint: IBreakpoint, debugService: IDebugService, debugSettings: IDebugConfiguration): IModelDecorationOptions { - const { className, message } = getBreakpointMessageAndClassName(debugService, breakpoint); +function getBreakpointDecorationOptions(model: ITextModel, breakpoint: IBreakpoint, state: State, breakpointsActivated: boolean, showBreakpointsInOverviewRuler: boolean): IModelDecorationOptions { + const { className, message } = getBreakpointMessageAndClassName(state, breakpointsActivated, breakpoint); let glyphMarginHoverMessage: MarkdownString | undefined; if (message) { @@ -85,14 +84,12 @@ function getBreakpointDecorationOptions(model: ITextModel, breakpoint: IBreakpoi } } - let overviewRulerDecoration: IModelDecorationOverviewRulerOptions | null; - if (debugSettings.showBreakpointsInOverviewRuler) { + let overviewRulerDecoration: IModelDecorationOverviewRulerOptions | null = null; + if (showBreakpointsInOverviewRuler) { overviewRulerDecoration = { color: 'rgb(124, 40, 49)', position: OverviewRulerLane.Left }; - } else { - overviewRulerDecoration = null; } const renderInline = breakpoint.column && (breakpoint.column > model.getLineFirstNonWhitespaceColumn(breakpoint.lineNumber)); @@ -105,11 +102,10 @@ function getBreakpointDecorationOptions(model: ITextModel, breakpoint: IBreakpoi }; } -async function createCandidateDecorations(model: ITextModel, breakpointDecorations: IBreakpointDecoration[], debugService: IDebugService): Promise<{ range: Range; options: IModelDecorationOptions; breakpoint: IBreakpoint | undefined }[]> { +async function createCandidateDecorations(model: ITextModel, breakpointDecorations: IBreakpointDecoration[], session: IDebugSession): Promise<{ range: Range; options: IModelDecorationOptions; breakpoint: IBreakpoint | undefined }[]> { const lineNumbers = distinct(breakpointDecorations.map(bpd => bpd.range.startLineNumber)); const result: { range: Range; options: IModelDecorationOptions; breakpoint: IBreakpoint | undefined }[] = []; - const session = debugService.getViewModel().focusedSession; - if (session && session.capabilities.supportsBreakpointLocationsRequest) { + if (session.capabilities.supportsBreakpointLocationsRequest) { await Promise.all(lineNumbers.map(async lineNumber => { try { const positions = await session.breakpointsLocations(model.uri, lineNumber); @@ -148,7 +144,7 @@ async function createCandidateDecorations(model: ITextModel, breakpointDecoratio return result; } -class BreakpointEditorContribution implements IBreakpointEditorContribution { +export class BreakpointEditorContribution implements IBreakpointEditorContribution { private breakpointHintDecoration: string[] = []; private breakpointWidget: BreakpointWidget | undefined; @@ -403,7 +399,7 @@ class BreakpointEditorContribution implements IBreakpointEditorContribution { const model = activeCodeEditor.getModel(); const breakpoints = this.debugService.getModel().getBreakpoints({ uri: model.uri }); const debugSettings = this.configurationService.getValue('debug'); - const desiredBreakpointDecorations = createBreakpointDecorations(model, breakpoints, this.debugService, debugSettings); + const desiredBreakpointDecorations = createBreakpointDecorations(model, breakpoints, this.debugService.state, this.debugService.getModel().areBreakpointsActivated(), debugSettings.showBreakpointsInOverviewRuler); try { this.ignoreDecorationsChangedEvent = true; @@ -436,7 +432,8 @@ class BreakpointEditorContribution implements IBreakpointEditorContribution { } // Set breakpoint candidate decorations - const desiredCandidateDecorations = debugSettings.showInlineBreakpointCandidates ? await createCandidateDecorations(this.editor.getModel(), this.breakpointDecorations, this.debugService) : []; + const session = this.debugService.getViewModel().focusedSession; + const desiredCandidateDecorations = debugSettings.showInlineBreakpointCandidates && session ? await createCandidateDecorations(this.editor.getModel(), this.breakpointDecorations, session) : []; const candidateDecorationIds = this.editor.deltaDecorations(this.candidateDecorations.map(c => c.decorationId), desiredCandidateDecorations); this.candidateDecorations.forEach(candidate => { candidate.inlineWidget.dispose(); @@ -446,7 +443,7 @@ class BreakpointEditorContribution implements IBreakpointEditorContribution { // Candidate decoration has a breakpoint attached when a breakpoint is already at that location and we did not yet set a decoration there // In practice this happens for the first breakpoint that was set on a line // We could have also rendered this first decoration as part of desiredBreakpointDecorations however at that moment we have no location information - const cssClass = candidate.breakpoint ? getBreakpointMessageAndClassName(this.debugService, candidate.breakpoint).className : 'codicon-debug-breakpoint-disabled'; + const cssClass = candidate.breakpoint ? getBreakpointMessageAndClassName(this.debugService.state, this.debugService.getModel().areBreakpointsActivated(), candidate.breakpoint).className : 'codicon-debug-breakpoint-disabled'; const contextMenuActions = () => this.getContextMenuActions(candidate.breakpoint ? [candidate.breakpoint] : [], activeCodeEditor.getModel().uri, candidate.range.startLineNumber, candidate.range.startColumn); const inlineWidget = new InlineBreakpointWidget(activeCodeEditor, decorationId, cssClass, candidate.breakpoint, this.debugService, this.contextMenuService, contextMenuActions); @@ -471,6 +468,7 @@ class BreakpointEditorContribution implements IBreakpointEditorContribution { const newBreakpointRange = model.getDecorationRange(breakpointDecoration.decorationId); if (newBreakpointRange && (!breakpointDecoration.range.equalsRange(newBreakpointRange))) { somethingChanged = true; + breakpointDecoration.range = newBreakpointRange; } }); if (!somethingChanged) { @@ -694,5 +692,3 @@ const debugIconBreakpointDisabledForeground = registerColor('debugIcon.breakpoin const debugIconBreakpointUnverifiedForeground = registerColor('debugIcon.breakpointUnverifiedForeground', { dark: '#848484', light: '#848484', hc: '#848484' }, nls.localize('debugIcon.breakpointUnverifiedForeground', 'Icon color for unverified breakpoints.')); const debugIconBreakpointCurrentStackframeForeground = registerColor('debugIcon.breakpointCurrentStackframeForeground', { dark: '#FFCC00', light: '#FFCC00', hc: '#FFCC00' }, nls.localize('debugIcon.breakpointCurrentStackframeForeground', 'Icon color for the current breakpoint stack frame.')); const debugIconBreakpointStackframeForeground = registerColor('debugIcon.breakpointStackframeForeground', { dark: '#89D185', light: '#89D185', hc: '#89D185' }, nls.localize('debugIcon.breakpointStackframeForeground', 'Icon color for all breakpoint stack frames.')); - -registerEditorContribution(BREAKPOINT_EDITOR_CONTRIBUTION_ID, BreakpointEditorContribution); diff --git a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts index b55a4134c0..ef7d501926 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts @@ -15,7 +15,7 @@ import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/zoneWidget'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IDebugService, IBreakpoint, BreakpointWidgetContext as Context, CONTEXT_BREAKPOINT_WIDGET_VISIBLE, DEBUG_SCHEME, CONTEXT_IN_BREAKPOINT_WIDGET, IBreakpointUpdateData, IBreakpointEditorContribution, BREAKPOINT_EDITOR_CONTRIBUTION_ID } from 'vs/workbench/contrib/debug/common/debug'; import { attachSelectBoxStyler } from 'vs/platform/theme/common/styler'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService'; import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor, EditorCommand, registerEditorCommand } from 'vs/editor/browser/editorExtensions'; @@ -46,6 +46,34 @@ export interface IPrivateBreakpointWidgetService { } const DECORATION_KEY = 'breakpointwidgetdecoration'; +function isCurlyBracketOpen(input: IActiveCodeEditor): boolean { + const model = input.getModel(); + const prevBracket = model.findPrevBracket(input.getPosition()); + if (prevBracket && prevBracket.isOpen) { + return true; + } + + return false; +} + +function createDecorations(theme: ITheme, placeHolder: string): IDecorationOptions[] { + const transparentForeground = transparent(editorForeground, 0.4)(theme); + return [{ + range: { + startLineNumber: 0, + endLineNumber: 0, + startColumn: 0, + endColumn: 1 + }, + renderOptions: { + after: { + contentText: placeHolder, + color: transparentForeground ? transparentForeground.toString() : undefined + } + } + }]; +} + export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWidgetService { _serviceBrand: undefined; @@ -197,7 +225,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi this.toDispose.push(model); const setDecorations = () => { const value = this.input.getModel().getValue(); - const decorations = !!value ? [] : this.createDecorations(); + const decorations = !!value ? [] : createDecorations(this.themeService.getTheme(), this.placeholder); this.input.setDecorations(DECORATION_KEY, decorations); }; this.input.getModel().onDidChangeContent(() => setDecorations()); @@ -207,7 +235,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi provideCompletionItems: (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken): Promise => { let suggestionsPromise: Promise; const underlyingModel = this.editor.getModel(); - if (underlyingModel && (this.context === Context.CONDITION || this.context === Context.LOG_MESSAGE && this.isCurlyBracketOpen())) { + if (underlyingModel && (this.context === Context.CONDITION || (this.context === Context.LOG_MESSAGE && isCurlyBracketOpen(this.input)))) { suggestionsPromise = provideSuggestionItems(underlyingModel, new Position(this.lineNumber, 1), new CompletionOptions(undefined, new Set().add(CompletionItemKind.Snippet)), _context, token).then(suggestions => { let overwriteBefore = 0; @@ -260,42 +288,6 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi } } - private createDecorations(): IDecorationOptions[] { - const transparentForeground = transparent(editorForeground, 0.4)(this.themeService.getTheme()); - return [{ - range: { - startLineNumber: 0, - endLineNumber: 0, - startColumn: 0, - endColumn: 1 - }, - renderOptions: { - after: { - contentText: this.placeholder, - color: transparentForeground ? transparentForeground.toString() : undefined - } - } - }]; - } - - private isCurlyBracketOpen(): boolean { - const value = this.input.getModel().getValue(); - const position = this.input.getPosition(); - if (position) { - for (let i = position.column - 2; i >= 0; i--) { - if (value[i] === '{') { - return true; - } - - if (value[i] === '}') { - return false; - } - } - } - - return false; - } - close(success: boolean): void { if (success) { // if there is already a breakpoint on this location - remove it. diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index 0b6f033fbc..9a851e8de6 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import * as resources from 'vs/base/common/resources'; import * as dom from 'vs/base/browser/dom'; import { IAction, Action } from 'vs/base/common/actions'; -import { IDebugService, IBreakpoint, CONTEXT_BREAKPOINTS_FOCUSED, State, DEBUG_SCHEME, IFunctionBreakpoint, IExceptionBreakpoint, IEnablement, BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, IBreakpoint, CONTEXT_BREAKPOINTS_FOCUSED, State, DEBUG_SCHEME, IFunctionBreakpoint, IExceptionBreakpoint, IEnablement, BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution, IDebugModel, IDataBreakpoint } from 'vs/workbench/contrib/debug/common/debug'; import { ExceptionBreakpoint, FunctionBreakpoint, Breakpoint, DataBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; import { AddFunctionBreakpointAction, ToggleBreakpointsActivatedAction, RemoveAllBreakpointsAction, RemoveBreakpointAction, EnableAllBreakpointsAction, DisableAllBreakpointsAction, ReapplyBreakpointsAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; @@ -45,9 +45,14 @@ function createCheckbox(): HTMLInputElement { return checkbox; } +const MAX_VISIBLE_BREAKPOINTS = 9; +export function getExpandedBodySize(model: IDebugModel): number { + const length = model.getBreakpoints().length + model.getExceptionBreakpoints().length + model.getFunctionBreakpoints().length + model.getDataBreakpoints().length; + return Math.min(MAX_VISIBLE_BREAKPOINTS, length) * 22; +} + export class BreakpointsView extends ViewPane { - private static readonly MAX_VISIBLE_FILES = 9; private list!: WorkbenchList; private needsRefresh = false; @@ -56,16 +61,16 @@ export class BreakpointsView extends ViewPane { @IContextMenuService contextMenuService: IContextMenuService, @IDebugService private readonly debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService, - @IInstantiationService private readonly instantiationService: IInstantiationService, + @IInstantiationService instantiationService: IInstantiationService, @IThemeService private readonly themeService: IThemeService, @IEditorService private readonly editorService: IEditorService, @IContextViewService private readonly contextViewService: IContextViewService, @IConfigurationService configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService, ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('breakpointsSection', "Breakpoints Section") }, keybindingService, contextMenuService, configurationService, contextKeyService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('breakpointsSection', "Breakpoints Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, instantiationService); - this.minimumBodySize = this.maximumBodySize = this.getExpandedBodySize(); + this.minimumBodySize = this.maximumBodySize = getExpandedBodySize(this.debugService.getModel()); this._register(this.debugService.getModel().onDidChangeBreakpoints(() => this.onBreakpointsChange())); } @@ -215,7 +220,7 @@ export class BreakpointsView extends ViewPane { private onBreakpointsChange(): void { if (this.isBodyVisible()) { - this.minimumBodySize = this.getExpandedBodySize(); + this.minimumBodySize = getExpandedBodySize(this.debugService.getModel()); if (this.maximumBodySize < Number.POSITIVE_INFINITY) { this.maximumBodySize = this.minimumBodySize; } @@ -234,12 +239,6 @@ export class BreakpointsView extends ViewPane { return elements; } - - private getExpandedBodySize(): number { - const model = this.debugService.getModel(); - const length = model.getBreakpoints().length + model.getExceptionBreakpoints().length + model.getFunctionBreakpoints().length + model.getDataBreakpoints().length; - return Math.min(BreakpointsView.MAX_VISIBLE_FILES, length) * 22; - } } class BreakpointsDelegate implements IListVirtualDelegate { @@ -351,7 +350,7 @@ class BreakpointsRenderer implements IListRenderer { - return !(breakpoint instanceof FunctionBreakpoint) && !(breakpoint instanceof DataBreakpoint) && breakpoint.message ? text.concat(', ' + breakpoint.message) : text; + return ('message' in breakpoint && breakpoint.message) ? text.concat(', ' + breakpoint.message) : text; }; if (debugActive && !breakpoint.verified) { return { className: breakpoint instanceof DataBreakpoint ? 'codicon-debug-breakpoint-data-unverified' : breakpoint instanceof FunctionBreakpoint ? 'codicon-debug-breakpoint-function-unverified' : breakpoint.logMessage ? 'codicon-debug-breakpoint-log-unverified' : 'codicon-debug-breakpoint-unverified', - message: breakpoint.message || (breakpoint.logMessage ? nls.localize('unverifiedLogpoint', "Unverified Logpoint") : nls.localize('unverifiedBreakopint', "Unverified Breakpoint")), + message: ('message' in breakpoint && breakpoint.message) ? breakpoint.message : (breakpoint.logMessage ? nls.localize('unverifiedLogpoint', "Unverified Logpoint") : nls.localize('unverifiedBreakopint', "Unverified Breakpoint")), }; } @@ -715,6 +713,6 @@ export function getBreakpointMessageAndClassName(debugService: IDebugService, br return { className: 'codicon-debug-breakpoint', - message: breakpoint.message || nls.localize('breakpoint', "Breakpoint") + message: ('message' in breakpoint && breakpoint.message) ? breakpoint.message : nls.localize('breakpoint', "Breakpoint") }; } diff --git a/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts index 4dd38fff06..3e7ac354e7 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Constants } from 'vs/base/common/uint'; -import { Range } from 'vs/editor/common/core/range'; +import { Range, IRange } from 'vs/editor/common/core/range'; import { TrackedRangeStickiness, IModelDeltaDecoration, IModelDecorationOptions } from 'vs/editor/common/model'; import { IDebugService, IStackFrame } from 'vs/workbench/contrib/debug/common/debug'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; @@ -13,11 +13,75 @@ import { localize } from 'vs/nls'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; const stickiness = TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges; -class CallStackEditorContribution implements IEditorContribution { +// we need a separate decoration for glyph margin, since we do not want it on each line of a multi line statement. +const TOP_STACK_FRAME_MARGIN: IModelDecorationOptions = { + glyphMarginClassName: 'codicon-debug-stackframe', + stickiness +}; +const FOCUSED_STACK_FRAME_MARGIN: IModelDecorationOptions = { + glyphMarginClassName: 'codicon-debug-stackframe-focused', + stickiness +}; +const TOP_STACK_FRAME_DECORATION: IModelDecorationOptions = { + isWholeLine: true, + className: 'debug-top-stack-frame-line', + stickiness +}; +const TOP_STACK_FRAME_INLINE_DECORATION: IModelDecorationOptions = { + beforeContentClassName: 'debug-top-stack-frame-column' +}; +const FOCUSED_STACK_FRAME_DECORATION: IModelDecorationOptions = { + isWholeLine: true, + className: 'debug-focused-stack-frame-line', + stickiness +}; + +export function createDecorationsForStackFrame(stackFrame: IStackFrame, topStackFrameRange: IRange | undefined): IModelDeltaDecoration[] { + // only show decorations for the currently focused thread. + const result: IModelDeltaDecoration[] = []; + const columnUntilEOLRange = new Range(stackFrame.range.startLineNumber, stackFrame.range.startColumn, stackFrame.range.startLineNumber, Constants.MAX_SAFE_SMALL_INTEGER); + const range = new Range(stackFrame.range.startLineNumber, stackFrame.range.startColumn, stackFrame.range.startLineNumber, stackFrame.range.startColumn + 1); + + // compute how to decorate the editor. Different decorations are used if this is a top stack frame, focused stack frame, + // an exception or a stack frame that did not change the line number (we only decorate the columns, not the whole line). + const callStack = stackFrame.thread.getCallStack(); + if (callStack && callStack.length && stackFrame === callStack[0]) { + result.push({ + options: TOP_STACK_FRAME_MARGIN, + range + }); + + result.push({ + options: TOP_STACK_FRAME_DECORATION, + range: columnUntilEOLRange + }); + + if (topStackFrameRange && topStackFrameRange.startLineNumber === stackFrame.range.startLineNumber && topStackFrameRange.startColumn !== stackFrame.range.startColumn) { + result.push({ + options: TOP_STACK_FRAME_INLINE_DECORATION, + range: columnUntilEOLRange + }); + } + topStackFrameRange = columnUntilEOLRange; + } else { + result.push({ + options: FOCUSED_STACK_FRAME_MARGIN, + range + }); + + result.push({ + options: FOCUSED_STACK_FRAME_DECORATION, + range: columnUntilEOLRange + }); + } + + return result; +} + +export class CallStackEditorContribution implements IEditorContribution { private toDispose: IDisposable[] = []; private decorationIds: string[] = []; private topStackFrameRange: Range | undefined; @@ -52,7 +116,7 @@ class CallStackEditorContribution implements IEditorContribution { } if (candidateStackFrame && candidateStackFrame.source.uri.toString() === this.editor.getModel()?.uri.toString()) { - decorations.push(...this.createDecorationsForStackFrame(candidateStackFrame)); + decorations.push(...createDecorationsForStackFrame(candidateStackFrame, this.topStackFrameRange)); } } }); @@ -61,78 +125,6 @@ class CallStackEditorContribution implements IEditorContribution { return decorations; } - private createDecorationsForStackFrame(stackFrame: IStackFrame): IModelDeltaDecoration[] { - // only show decorations for the currently focused thread. - const result: IModelDeltaDecoration[] = []; - const columnUntilEOLRange = new Range(stackFrame.range.startLineNumber, stackFrame.range.startColumn, stackFrame.range.startLineNumber, Constants.MAX_SAFE_SMALL_INTEGER); - const range = new Range(stackFrame.range.startLineNumber, stackFrame.range.startColumn, stackFrame.range.startLineNumber, stackFrame.range.startColumn + 1); - - // compute how to decorate the editor. Different decorations are used if this is a top stack frame, focused stack frame, - // an exception or a stack frame that did not change the line number (we only decorate the columns, not the whole line). - const callStack = stackFrame.thread.getCallStack(); - if (callStack && callStack.length && stackFrame === callStack[0]) { - result.push({ - options: CallStackEditorContribution.TOP_STACK_FRAME_MARGIN, - range - }); - - result.push({ - options: CallStackEditorContribution.TOP_STACK_FRAME_DECORATION, - range: columnUntilEOLRange - }); - - if (this.topStackFrameRange && this.topStackFrameRange.startLineNumber === stackFrame.range.startLineNumber && this.topStackFrameRange.startColumn !== stackFrame.range.startColumn) { - result.push({ - options: CallStackEditorContribution.TOP_STACK_FRAME_INLINE_DECORATION, - range: columnUntilEOLRange - }); - } - this.topStackFrameRange = columnUntilEOLRange; - } else { - result.push({ - options: CallStackEditorContribution.FOCUSED_STACK_FRAME_MARGIN, - range - }); - - result.push({ - options: CallStackEditorContribution.FOCUSED_STACK_FRAME_DECORATION, - range: columnUntilEOLRange - }); - } - - return result; - } - - // editor decorations - - static readonly STICKINESS = TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges; - // we need a separate decoration for glyph margin, since we do not want it on each line of a multi line statement. - private static TOP_STACK_FRAME_MARGIN: IModelDecorationOptions = { - glyphMarginClassName: 'codicon-debug-stackframe', - stickiness - }; - - private static FOCUSED_STACK_FRAME_MARGIN: IModelDecorationOptions = { - glyphMarginClassName: 'codicon-debug-stackframe-focused', - stickiness - }; - - private static TOP_STACK_FRAME_DECORATION: IModelDecorationOptions = { - isWholeLine: true, - className: 'debug-top-stack-frame-line', - stickiness - }; - - private static TOP_STACK_FRAME_INLINE_DECORATION: IModelDecorationOptions = { - beforeContentClassName: 'debug-top-stack-frame-column' - }; - - private static FOCUSED_STACK_FRAME_DECORATION: IModelDecorationOptions = { - isWholeLine: true, - className: 'debug-focused-stack-frame-line', - stickiness - }; - dispose(): void { this.editor.deltaDecorations(this.decorationIds, []); this.toDispose = dispose(this.toDispose); @@ -154,5 +146,3 @@ registerThemingParticipant((theme, collector) => { const topStackFrameColor = registerColor('editor.stackFrameHighlightBackground', { dark: '#ffff0033', light: '#ffff6673', hc: '#fff600' }, localize('topStackFrameLineHighlight', 'Background color for the highlight of line at the top stack frame position.')); const focusedStackFrameColor = registerColor('editor.focusedStackFrameHighlightBackground', { dark: '#7abd7a4d', light: '#cee7ce73', hc: '#cee7ce' }, localize('focusedStackFrameLineHighlight', 'Background color for the highlight of line at focused stack frame position.')); - -registerEditorContribution('editor.contrib.callStack', CallStackEditorContribution); diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index 26785fa8b5..37fde1cbd1 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -40,7 +40,7 @@ const $ = dom.$; type CallStackItem = IStackFrame | IThread | IDebugSession | string | ThreadAndSessionIds | IStackFrame[]; -function getContext(element: CallStackItem | null): any { +export function getContext(element: CallStackItem | null): any { return element instanceof StackFrame ? { sessionId: element.thread.session.getId(), threadId: element.thread.getId(), @@ -53,6 +53,25 @@ function getContext(element: CallStackItem | null): any { } : undefined; } +// Extensions depend on this context, should not be changed even though it is not fully deterministic +export function getContextForContributedActions(element: CallStackItem | null): string | number { + if (element instanceof StackFrame) { + if (element.source.inMemory) { + return element.source.raw.path || element.source.reference || ''; + } + + return element.source.uri.toString(); + } + if (element instanceof Thread) { + return element.threadId; + } + if (isDebugSession(element)) { + return element.getId(); + } + + return ''; +} + export class CallStackView extends ViewPane { private pauseMessage!: HTMLSpanElement; private pauseMessageLabel!: HTMLSpanElement; @@ -72,13 +91,13 @@ export class CallStackView extends ViewPane { @IContextMenuService contextMenuService: IContextMenuService, @IDebugService private readonly debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService, - @IInstantiationService private readonly instantiationService: IInstantiationService, + @IInstantiationService instantiationService: IInstantiationService, @IEditorService private readonly editorService: IEditorService, @IConfigurationService configurationService: IConfigurationService, @IMenuService menuService: IMenuService, @IContextKeyService readonly contextKeyService: IContextKeyService, ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('callstackSection', "Call Stack Section") }, keybindingService, contextMenuService, configurationService, contextKeyService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('callstackSection', "Call Stack Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, instantiationService); this.callStackItemType = CONTEXT_CALLSTACK_ITEM_TYPE.bindTo(contextKeyService); this.contributedContextMenu = menuService.createMenu(MenuId.DebugCallStackContext, contextKeyService); @@ -289,7 +308,13 @@ export class CallStackView extends ViewPane { this.ignoreSelectionChangedEvent = true; try { this.tree.setSelection([element]); - this.tree.reveal(element); + // If the element is outside of the screen bounds, + // position it in the middle + if (this.tree.getRelativeTop(element) === null) { + this.tree.reveal(element, 0.5); + } else { + this.tree.reveal(element); + } } catch (e) { } finally { this.ignoreSelectionChangedEvent = false; @@ -336,7 +361,7 @@ export class CallStackView extends ViewPane { } const actions: IAction[] = []; - const actionsDisposable = createAndFillInContextMenuActions(this.contributedContextMenu, { arg: this.getContextForContributedActions(element), shouldForwardArgs: true }, actions, this.contextMenuService); + const actionsDisposable = createAndFillInContextMenuActions(this.contributedContextMenu, { arg: getContextForContributedActions(element), shouldForwardArgs: true }, actions, this.contextMenuService); this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, @@ -345,24 +370,6 @@ export class CallStackView extends ViewPane { onHide: () => dispose(actionsDisposable) }); } - - private getContextForContributedActions(element: CallStackItem | null): string | number { - if (element instanceof StackFrame) { - if (element.source.inMemory) { - return element.source.raw.path || element.source.reference || ''; - } - - return element.source.uri.toString(); - } - if (element instanceof Thread) { - return element.threadId; - } - if (isDebugSession(element)) { - return element.getId(); - } - - return ''; - } } interface IThreadTemplateData { @@ -514,7 +521,7 @@ class StackFramesRenderer implements ITreeRenderer(ViewExtensions.ViewContainersRegistry).registerViewContainer({ id: VIEWLET_ID, - name: nls.localize('debugAndRun', "Debug and Run"), - ctorDescriptor: { ctor: DebugViewPaneContainer }, + name: nls.localize('runAndDebug', "Run and Debug"), + ctorDescriptor: new SyncDescriptor(DebugViewPaneContainer), icon: 'codicon-debug-alt', order: 13 // {{SQL CARBON EDIT}} }, ViewContainerLocation.Sidebar); @@ -95,23 +84,32 @@ const openPanelKb: IKeybindings = { }; // register repl panel -Registry.as(PanelExtensions.Panels).registerPanel(PanelDescriptor.create( - Repl, - REPL_ID, - nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'debugPanel' }, 'Debug Console'), - 'repl', - 30, - OpenDebugPanelAction.ID -)); + +const VIEW_CONTAINER: ViewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ + id: DEBUG_PANEL_ID, + name: nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'debugPanel' }, 'Debug Console'), + ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [DEBUG_PANEL_ID, DEBUG_PANEL_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), + focusCommand: { + id: OpenDebugPanelAction.ID, + keybindings: openPanelKb + } +}, ViewContainerLocation.Panel); + +Registry.as(ViewExtensions.ViewsRegistry).registerViews([{ + id: REPL_VIEW_ID, + name: nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'debugPanel' }, 'Debug Console'), + canToggleVisibility: false, + ctorDescriptor: new SyncDescriptor(Repl), +}], VIEW_CONTAINER); // Register default debug views const viewsRegistry = Registry.as(ViewExtensions.ViewsRegistry); -viewsRegistry.registerViews([{ id: VARIABLES_VIEW_ID, name: nls.localize('variables', "Variables"), ctorDescriptor: { ctor: VariablesView }, order: 10, weight: 40, canToggleVisibility: true, focusCommand: { id: 'workbench.debug.action.focusVariablesView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); -viewsRegistry.registerViews([{ id: WATCH_VIEW_ID, name: nls.localize('watch', "Watch"), ctorDescriptor: { ctor: WatchExpressionsView }, order: 20, weight: 10, canToggleVisibility: true, focusCommand: { id: 'workbench.debug.action.focusWatchView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); -viewsRegistry.registerViews([{ id: CALLSTACK_VIEW_ID, name: nls.localize('callStack', "Call Stack"), ctorDescriptor: { ctor: CallStackView }, order: 30, weight: 30, canToggleVisibility: true, focusCommand: { id: 'workbench.debug.action.focusCallStackView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); -viewsRegistry.registerViews([{ id: BREAKPOINTS_VIEW_ID, name: nls.localize('breakpoints', "Breakpoints"), ctorDescriptor: { ctor: BreakpointsView }, order: 40, weight: 20, canToggleVisibility: true, focusCommand: { id: 'workbench.debug.action.focusBreakpointsView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); -viewsRegistry.registerViews([{ id: StartView.ID, name: StartView.LABEL, ctorDescriptor: { ctor: StartView }, order: 10, weight: 40, canToggleVisibility: true, when: CONTEXT_DEBUG_UX.isEqualTo('simple') }], viewContainer); -viewsRegistry.registerViews([{ id: LOADED_SCRIPTS_VIEW_ID, name: nls.localize('loadedScripts', "Loaded Scripts"), ctorDescriptor: { ctor: LoadedScriptsView }, order: 35, weight: 5, canToggleVisibility: true, collapsed: true, when: ContextKeyExpr.and(CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_DEBUG_UX.isEqualTo('default')) }], viewContainer); +viewsRegistry.registerViews([{ id: VARIABLES_VIEW_ID, name: nls.localize('variables', "Variables"), ctorDescriptor: new SyncDescriptor(VariablesView), order: 10, weight: 40, canToggleVisibility: true, focusCommand: { id: 'workbench.debug.action.focusVariablesView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); +viewsRegistry.registerViews([{ id: WATCH_VIEW_ID, name: nls.localize('watch', "Watch"), ctorDescriptor: new SyncDescriptor(WatchExpressionsView), order: 20, weight: 10, canToggleVisibility: true, focusCommand: { id: 'workbench.debug.action.focusWatchView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); +viewsRegistry.registerViews([{ id: CALLSTACK_VIEW_ID, name: nls.localize('callStack', "Call Stack"), ctorDescriptor: new SyncDescriptor(CallStackView), order: 30, weight: 30, canToggleVisibility: true, focusCommand: { id: 'workbench.debug.action.focusCallStackView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); +viewsRegistry.registerViews([{ id: BREAKPOINTS_VIEW_ID, name: nls.localize('breakpoints', "Breakpoints"), ctorDescriptor: new SyncDescriptor(BreakpointsView), order: 40, weight: 20, canToggleVisibility: true, focusCommand: { id: 'workbench.debug.action.focusBreakpointsView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); +viewsRegistry.registerViews([{ id: StartView.ID, name: StartView.LABEL, ctorDescriptor: new SyncDescriptor(StartView), order: 10, weight: 40, canToggleVisibility: true, when: CONTEXT_DEBUG_UX.isEqualTo('simple') }], viewContainer); +viewsRegistry.registerViews([{ id: LOADED_SCRIPTS_VIEW_ID, name: nls.localize('loadedScripts', "Loaded Scripts"), ctorDescriptor: new SyncDescriptor(LoadedScriptsView), order: 35, weight: 5, canToggleVisibility: true, collapsed: true, when: ContextKeyExpr.and(CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_DEBUG_UX.isEqualTo('default')) }], viewContainer); registerCommands(); @@ -212,6 +210,11 @@ configurationRegistry.registerConfiguration({ default: 'onFirstSessionStart' }, 'debug.internalConsoleOptions': INTERNAL_CONSOLE_OPTIONS_SCHEMA, + 'debug.console.closeOnEnd': { + type: 'boolean', + description: nls.localize('debug.console.closeOnEnd', "Controls if the debug console should be automatically closed when the debug session ends."), + default: false + }, 'debug.openDebug': { enum: ['neverOpen', 'openOnSessionStart', 'openOnFirstSessionStart', 'openOnDebugBreak'], default: 'openOnSessionStart', @@ -331,6 +334,11 @@ registerDebugCallstackItem(TERMINATE_THREAD_ID, nls.localize('terminateThread', registerDebugCallstackItem(RESTART_FRAME_ID, nls.localize('restartFrame', "Restart Frame"), 10, ContextKeyExpr.and(CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('stackFrame'), CONTEXT_RESTART_FRAME_SUPPORTED)); registerDebugCallstackItem(COPY_STACK_TRACE_ID, nls.localize('copyStackTrace', "Copy Call Stack"), 20, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('stackFrame')); +// Editor contributions + +registerEditorContribution('editor.contrib.callStack', CallStackEditorContribution); +registerEditorContribution(BREAKPOINT_EDITOR_CONTRIBUTION_ID, BreakpointEditorContribution); + // View menu // {{SQL CARBON EDIT}} - Disable unused menu item diff --git a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts index ff5919768d..bc024a4ea0 100644 --- a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts +++ b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts @@ -161,9 +161,6 @@ export class StartDebugActionViewItem implements IActionViewItem { let lastGroup: string | undefined; const disabledIdxs: number[] = []; manager.getAllConfigurations().forEach(({ launch, name, presentation }) => { - if (name === manager.selectedConfiguration.name && launch === manager.selectedConfiguration.launch) { - this.selected = this.options.length; - } if (lastGroup !== presentation?.group) { lastGroup = presentation?.group; if (this.options.length) { @@ -171,6 +168,9 @@ export class StartDebugActionViewItem implements IActionViewItem { disabledIdxs.push(this.options.length - 1); } } + if (name === manager.selectedConfiguration.name && launch === manager.selectedConfiguration.launch) { + this.selected = this.options.length; + } const label = inWorkspace ? `${name} (${launch.name})` : name; this.options.push({ label, handler: () => { manager.selectConfiguration(launch, name); return true; } }); diff --git a/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/src/vs/workbench/contrib/debug/browser/debugCommands.ts index d5af7dbd48..74562983fd 100644 --- a/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -9,7 +9,7 @@ import { List } from 'vs/base/browser/ui/list/listWidget'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IListService } from 'vs/platform/list/browser/listService'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IDebugService, IEnablement, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_VARIABLES_FOCUSED, EDITOR_CONTRIBUTION_ID, IDebugEditorContribution, CONTEXT_IN_DEBUG_MODE, CONTEXT_EXPRESSION_SELECTED, CONTEXT_BREAKPOINT_SELECTED, IConfig, IStackFrame, IThread, IDebugSession, CONTEXT_DEBUG_STATE, REPL_ID, IDebugConfiguration, CONTEXT_JUMP_TO_CURSOR_SUPPORTED } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, IEnablement, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_VARIABLES_FOCUSED, EDITOR_CONTRIBUTION_ID, IDebugEditorContribution, CONTEXT_IN_DEBUG_MODE, CONTEXT_EXPRESSION_SELECTED, CONTEXT_BREAKPOINT_SELECTED, IConfig, IStackFrame, IThread, IDebugSession, CONTEXT_DEBUG_STATE, IDebugConfiguration, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, REPL_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; import { Expression, Variable, Breakpoint, FunctionBreakpoint, DataBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; import { IExtensionsViewPaneContainer, VIEWLET_ID as EXTENSIONS_VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; @@ -27,9 +27,9 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { onUnexpectedError } from 'vs/base/common/errors'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { IViewsService } from 'vs/workbench/common/views'; export const ADD_CONFIGURATION_ID = 'debug.addConfiguration'; export const TOGGLE_INLINE_BREAKPOINT_ID = 'editor.debug.action.toggleInlineBreakpoint'; @@ -239,7 +239,8 @@ export function registerCommands(): void { id: STEP_INTO_ID, weight: KeybindingWeight.WorkbenchContrib + 10, // Have a stronger weight to have priority over full screen when debugging primary: KeyCode.F11, - when: CONTEXT_DEBUG_STATE.isEqualTo('stopped'), + // Use a more flexible when clause to not allow full screen command to take over when F11 pressed a lot of times + when: CONTEXT_DEBUG_STATE.notEqualsTo('inactive'), handler: (accessor: ServicesAccessor, _: string, context: CallStackContext | unknown) => { getThreadAndRun(accessor, context, (thread: IThread) => thread.stepIn()); } @@ -303,9 +304,14 @@ export function registerCommands(): void { id: RESTART_FRAME_ID, handler: async (accessor: ServicesAccessor, _: string, context: CallStackContext | unknown) => { const debugService = accessor.get(IDebugService); + const notificationService = accessor.get(INotificationService); let frame = getFrame(debugService, context); if (frame) { - await frame.restart(); + try { + await frame.restart(); + } catch (e) { + notificationService.error(e); + } } } }); @@ -322,9 +328,9 @@ export function registerCommands(): void { CommandsRegistry.registerCommand({ id: FOCUS_REPL_ID, - handler: (accessor) => { - const panelService = accessor.get(IPanelService); - panelService.openPanel(REPL_ID, true); + handler: async (accessor) => { + const viewsService = accessor.get(IViewsService); + await viewsService.openView(REPL_VIEW_ID, true); } }); diff --git a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts index 59d93cd00f..4bd1f73d30 100644 --- a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts @@ -38,6 +38,7 @@ import { withUndefinedAsNull } from 'vs/base/common/types'; import { sequence } from 'vs/base/common/async'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { first } from 'vs/base/common/arrays'; +import { getVisibleAndSorted } from 'vs/workbench/contrib/debug/common/debugUtils'; const jsonRegistry = Registry.as(JSONExtensions.JSONContribution); jsonRegistry.registerSchema(launchSchemaId, launchSchema); @@ -207,6 +208,22 @@ export class ConfigurationManager implements IConfigurationManager { return result; } + async resolveDebugConfigurationWithSubstitutedVariables(folderUri: uri | undefined, type: string | undefined, config: IConfig, token: CancellationToken): Promise { + // pipe the config through the promises sequentially. Append at the end the '*' types + const providers = this.configProviders.filter(p => p.type === type && p.resolveDebugConfigurationWithSubstitutedVariables) + .concat(this.configProviders.filter(p => p.type === '*' && p.resolveDebugConfigurationWithSubstitutedVariables)); + + let result: IConfig | null | undefined = config; + await sequence(providers.map(provider => async () => { + // If any provider returned undefined or null make sure to respect that and do not pass the result to more resolver + if (result) { + result = await provider.resolveDebugConfigurationWithSubstitutedVariables!(folderUri, result, token); + } + })); + + return result; + } + async provideDebugConfigurations(folderUri: uri | undefined, type: string, token: CancellationToken): Promise { await this.activateDebuggers('onDebugInitialConfigurations'); const results = await Promise.all(this.configProviders.filter(p => p.type === type && p.provideDebugConfigurations).map(p => p.provideDebugConfigurations!(folderUri, token))); @@ -219,36 +236,13 @@ export class ConfigurationManager implements IConfigurationManager { for (let l of this.launches) { for (let name of l.getConfigurationNames()) { const config = l.getConfiguration(name) || l.getCompound(name); - if (config && !config.presentation?.hidden) { + if (config) { all.push({ launch: l, name, presentation: config.presentation }); } } } - return all.sort((first, second) => { - if (!first.presentation) { - return 1; - } - if (!second.presentation) { - return -1; - } - if (!first.presentation.group) { - return 1; - } - if (!second.presentation.group) { - return -1; - } - if (first.presentation.group !== second.presentation.group) { - return first.presentation.group.localeCompare(second.presentation.group); - } - if (typeof first.presentation.order !== 'number') { - return 1; - } - if (typeof second.presentation.order !== 'number') { - return -1; - } - return first.presentation.order - second.presentation.order; - }); + return getVisibleAndSorted(all); } private registerListeners(): void { diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts index e9375bd487..dca5551774 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts @@ -9,14 +9,14 @@ import { Range } from 'vs/editor/common/core/range'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ServicesAccessor, registerEditorAction, EditorAction, IActionOptions } from 'vs/editor/browser/editorExtensions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IDebugService, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE, State, REPL_ID, VIEWLET_ID, IDebugEditorContribution, EDITOR_CONTRIBUTION_ID, BreakpointWidgetContext, IBreakpoint, BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution } from 'vs/workbench/contrib/debug/common/debug'; -import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; +import { IDebugService, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE, State, VIEWLET_ID, IDebugEditorContribution, EDITOR_CONTRIBUTION_ID, BreakpointWidgetContext, IBreakpoint, BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution, REPL_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { openBreakpointSource } from 'vs/workbench/contrib/debug/browser/breakpointsView'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { PanelFocusContext } from 'vs/workbench/common/panel'; +import { IViewsService } from 'vs/workbench/common/views'; export const TOGGLE_BREAKPOINT_ID = 'editor.debug.action.toggleBreakpoint'; class ToggleBreakpointAction extends EditorAction { @@ -169,7 +169,7 @@ class SelectionToReplAction extends EditorAction { async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { const debugService = accessor.get(IDebugService); - const panelService = accessor.get(IPanelService); + const viewsService = accessor.get(IViewsService); const viewModel = debugService.getViewModel(); const session = viewModel.focusedSession; if (!editor.hasModel() || !session) { @@ -178,7 +178,7 @@ class SelectionToReplAction extends EditorAction { const text = editor.getModel().getValueInRange(editor.getSelection()); await session.addReplExpression(viewModel.focusedStackFrame!, text); - await panelService.openPanel(REPL_ID, true); + await viewsService.openView(REPL_VIEW_ID, true); } } diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index d2c27cd57d..0a36fb337b 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -27,7 +27,7 @@ import { FloatingClickWidget } from 'vs/workbench/browser/parts/editor/editorWid import { Position } from 'vs/editor/common/core/position'; import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; import { first } from 'vs/base/common/arrays'; -import { memoize } from 'vs/base/common/decorators'; +import { memoize, createMemoizer } from 'vs/base/common/decorators'; import { IEditorHoverOptions, EditorOption } from 'vs/editor/common/config/editorOptions'; import { CancellationToken } from 'vs/base/common/cancellation'; import { DebugHoverWidget } from 'vs/workbench/contrib/debug/browser/debugHover'; @@ -36,12 +36,131 @@ import { getHover } from 'vs/editor/contrib/hover/getHover'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; const HOVER_DELAY = 300; -const LAUNCH_JSON_REGEX = /launch\.json$/; +const LAUNCH_JSON_REGEX = /\.vscode\/launch\.json$/; const INLINE_VALUE_DECORATION_KEY = 'inlinevaluedecoration'; const MAX_NUM_INLINE_VALUES = 100; // JS Global scope can have 700+ entries. We want to limit ourselves for perf reasons const MAX_INLINE_DECORATOR_LENGTH = 150; // Max string length of each inline decorator when debugging. If exceeded ... is added const MAX_TOKENIZATION_LINE_LEN = 500; // If line is too long, then inline values for the line are skipped +function createInlineValueDecoration(lineNumber: number, contentText: string): IDecorationOptions { + // If decoratorText is too long, trim and add ellipses. This could happen for minified files with everything on a single line + if (contentText.length > MAX_INLINE_DECORATOR_LENGTH) { + contentText = contentText.substr(0, MAX_INLINE_DECORATOR_LENGTH) + '...'; + } + + return { + range: { + startLineNumber: lineNumber, + endLineNumber: lineNumber, + startColumn: Constants.MAX_SAFE_SMALL_INTEGER, + endColumn: Constants.MAX_SAFE_SMALL_INTEGER + }, + renderOptions: { + after: { + contentText, + backgroundColor: 'rgba(255, 200, 0, 0.2)', + margin: '10px' + }, + dark: { + after: { + color: 'rgba(255, 255, 255, 0.5)', + } + }, + light: { + after: { + color: 'rgba(0, 0, 0, 0.5)', + } + } + } + }; +} + +function createInlineValueDecorationsInsideRange(expressions: ReadonlyArray, range: Range, model: ITextModel, wordToLineNumbersMap: Map): IDecorationOptions[] { + const nameValueMap = new Map(); + for (let expr of expressions) { + nameValueMap.set(expr.name, expr.value); + // Limit the size of map. Too large can have a perf impact + if (nameValueMap.size >= MAX_NUM_INLINE_VALUES) { + break; + } + } + + const lineToNamesMap: Map = new Map(); + + // Compute unique set of names on each line + nameValueMap.forEach((_value, name) => { + const lineNumbers = wordToLineNumbersMap.get(name); + if (lineNumbers) { + for (let lineNumber of lineNumbers) { + if (range.containsPosition(new Position(lineNumber, 0))) { + if (!lineToNamesMap.has(lineNumber)) { + lineToNamesMap.set(lineNumber, []); + } + + if (lineToNamesMap.get(lineNumber)!.indexOf(name) === -1) { + lineToNamesMap.get(lineNumber)!.push(name); + } + } + } + } + }); + + const decorations: IDecorationOptions[] = []; + // Compute decorators for each line + lineToNamesMap.forEach((names, line) => { + const contentText = names.sort((first, second) => { + const content = model.getLineContent(line); + return content.indexOf(first) - content.indexOf(second); + }).map(name => `${name} = ${nameValueMap.get(name)}`).join(', '); + decorations.push(createInlineValueDecoration(line, contentText)); + }); + + return decorations; +} + +function getWordToLineNumbersMap(model: ITextModel | null): Map { + const result = new Map(); + if (!model) { + return result; + } + + // For every word in every line, map its ranges for fast lookup + for (let lineNumber = 1, len = model.getLineCount(); lineNumber <= len; ++lineNumber) { + const lineContent = model.getLineContent(lineNumber); + + // If line is too long then skip the line + if (lineContent.length > MAX_TOKENIZATION_LINE_LEN) { + continue; + } + + model.forceTokenization(lineNumber); + const lineTokens = model.getLineTokens(lineNumber); + for (let tokenIndex = 0, tokenCount = lineTokens.getCount(); tokenIndex < tokenCount; tokenIndex++) { + const tokenStartOffset = lineTokens.getStartOffset(tokenIndex); + const tokenEndOffset = lineTokens.getEndOffset(tokenIndex); + const tokenType = lineTokens.getStandardTokenType(tokenIndex); + const tokenStr = lineContent.substring(tokenStartOffset, tokenEndOffset); + + // Token is a word and not a comment + if (tokenType === StandardTokenType.Other) { + DEFAULT_WORD_REGEXP.lastIndex = 0; // We assume tokens will usually map 1:1 to words if they match + const wordMatch = DEFAULT_WORD_REGEXP.exec(tokenStr); + + if (wordMatch) { + const word = wordMatch[0]; + if (!result.has(word)) { + result.set(word, []); + } + + result.get(word)!.push(lineNumber); + } + } + } + } + + return result; +} + class DebugEditorContribution implements IDebugEditorContribution { private toDispose: IDisposable[]; @@ -49,8 +168,7 @@ class DebugEditorContribution implements IDebugEditorContribution { private nonDebugHoverPosition: Position | undefined; private hoverRange: Range | null = null; private mouseDown = false; - - private wordToLineNumbersMap: Map | undefined; + private static readonly MEMOIZER = createMemoizer(); private exceptionWidget: ExceptionWidget | undefined; @@ -95,7 +213,7 @@ class DebugEditorContribution implements IDebugEditorContribution { })); this.toDispose.push(this.editor.onKeyDown((e: IKeyboardEvent) => this.onKeyDown(e))); this.toDispose.push(this.editor.onDidChangeModelContent(() => { - this.wordToLineNumbersMap = undefined; + DebugEditorContribution.MEMOIZER.clear(); this.updateInlineValuesScheduler.schedule(); })); this.toDispose.push(this.editor.onDidChangeModel(async () => { @@ -107,7 +225,7 @@ class DebugEditorContribution implements IDebugEditorContribution { this.toggleExceptionWidget(); this.hideHoverWidget(); this.updateConfigurationWidgetVisibility(); - this.wordToLineNumbersMap = undefined; + DebugEditorContribution.MEMOIZER.clear(); await this.updateInlineValueDecorations(stackFrame); })); this.toDispose.push(this.editor.onDidScrollChange(() => this.hideHoverWidget)); @@ -118,6 +236,11 @@ class DebugEditorContribution implements IDebugEditorContribution { })); } + @DebugEditorContribution.MEMOIZER + private get wordToLineNumbersMap(): Map { + return getWordToLineNumbersMap(this.editor.getModel()); + } + private _applyHoverConfiguration(model: ITextModel, stackFrame: IStackFrame | undefined): void { if (stackFrame && model.uri.toString() === stackFrame.source.uri.toString()) { this.editor.updateOptions({ @@ -237,6 +360,7 @@ class DebugEditorContribution implements IDebugEditorContribution { if (targetType === MouseTargetType.CONTENT_TEXT) { if (mouseEvent.target.range && !mouseEvent.target.range.equalsRange(this.hoverRange)) { this.hoverRange = mouseEvent.target.range; + this.hideHoverScheduler.cancel(); this.showHoverScheduler.schedule(); } } else if (!this.mouseDown) { @@ -400,136 +524,14 @@ class DebugEditorContribution implements IDebugEditorContribution { range = range.setStartPosition(scope.range.startLineNumber, scope.range.startColumn); } - return this.createInlineValueDecorationsInsideRange(children, range, model); + return createInlineValueDecorationsInsideRange(children, range, model, this.wordToLineNumbersMap); })); + const allDecorations = decorationsPerScope.reduce((previous, current) => previous.concat(current), []); this.editor.setDecorations(INLINE_VALUE_DECORATION_KEY, allDecorations); } - private createInlineValueDecorationsInsideRange(expressions: ReadonlyArray, range: Range, model: ITextModel): IDecorationOptions[] { - const nameValueMap = new Map(); - for (let expr of expressions) { - nameValueMap.set(expr.name, expr.value); - // Limit the size of map. Too large can have a perf impact - if (nameValueMap.size >= MAX_NUM_INLINE_VALUES) { - break; - } - } - - const lineToNamesMap: Map = new Map(); - const wordToPositionsMap = this.getWordToPositionsMap(); - - // Compute unique set of names on each line - nameValueMap.forEach((value, name) => { - const positions = wordToPositionsMap.get(name); - if (positions) { - for (let position of positions) { - if (range.containsPosition(position)) { - if (!lineToNamesMap.has(position.lineNumber)) { - lineToNamesMap.set(position.lineNumber, []); - } - - if (lineToNamesMap.get(position.lineNumber)!.indexOf(name) === -1) { - lineToNamesMap.get(position.lineNumber)!.push(name); - } - } - } - } - }); - - const decorations: IDecorationOptions[] = []; - // Compute decorators for each line - lineToNamesMap.forEach((names, line) => { - const contentText = names.sort((first, second) => { - const content = model.getLineContent(line); - return content.indexOf(first) - content.indexOf(second); - }).map(name => `${name} = ${nameValueMap.get(name)}`).join(', '); - decorations.push(this.createInlineValueDecoration(line, contentText)); - }); - - return decorations; - } - - private createInlineValueDecoration(lineNumber: number, contentText: string): IDecorationOptions { - // If decoratorText is too long, trim and add ellipses. This could happen for minified files with everything on a single line - if (contentText.length > MAX_INLINE_DECORATOR_LENGTH) { - contentText = contentText.substr(0, MAX_INLINE_DECORATOR_LENGTH) + '...'; - } - - return { - range: { - startLineNumber: lineNumber, - endLineNumber: lineNumber, - startColumn: Constants.MAX_SAFE_SMALL_INTEGER, - endColumn: Constants.MAX_SAFE_SMALL_INTEGER - }, - renderOptions: { - after: { - contentText, - backgroundColor: 'rgba(255, 200, 0, 0.2)', - margin: '10px' - }, - dark: { - after: { - color: 'rgba(255, 255, 255, 0.5)', - } - }, - light: { - after: { - color: 'rgba(0, 0, 0, 0.5)', - } - } - } - }; - } - - private getWordToPositionsMap(): Map { - if (!this.wordToLineNumbersMap) { - this.wordToLineNumbersMap = new Map(); - const model = this.editor.getModel(); - if (!model) { - return this.wordToLineNumbersMap; - } - - // For every word in every line, map its ranges for fast lookup - for (let lineNumber = 1, len = model.getLineCount(); lineNumber <= len; ++lineNumber) { - const lineContent = model.getLineContent(lineNumber); - - // If line is too long then skip the line - if (lineContent.length > MAX_TOKENIZATION_LINE_LEN) { - continue; - } - - model.forceTokenization(lineNumber); - const lineTokens = model.getLineTokens(lineNumber); - for (let tokenIndex = 0, tokenCount = lineTokens.getCount(); tokenIndex < tokenCount; tokenIndex++) { - const tokenStartOffset = lineTokens.getStartOffset(tokenIndex); - const tokenEndOffset = lineTokens.getEndOffset(tokenIndex); - const tokenType = lineTokens.getStandardTokenType(tokenIndex); - const tokenStr = lineContent.substring(tokenStartOffset, tokenEndOffset); - - // Token is a word and not a comment - if (tokenType === StandardTokenType.Other) { - DEFAULT_WORD_REGEXP.lastIndex = 0; // We assume tokens will usually map 1:1 to words if they match - const wordMatch = DEFAULT_WORD_REGEXP.exec(tokenStr); - - if (wordMatch) { - const word = wordMatch[0]; - if (!this.wordToLineNumbersMap.has(word)) { - this.wordToLineNumbersMap.set(word, []); - } - - this.wordToLineNumbersMap.get(word)!.push(new Position(lineNumber, tokenStartOffset)); - } - } - } - } - } - - return this.wordToLineNumbersMap; - } - dispose(): void { if (this.hoverWidget) { this.hoverWidget.dispose(); diff --git a/src/vs/workbench/contrib/debug/browser/debugHover.ts b/src/vs/workbench/contrib/debug/browser/debugHover.ts index 44bb809d01..1ffebe3b42 100644 --- a/src/vs/workbench/contrib/debug/browser/debugHover.ts +++ b/src/vs/workbench/contrib/debug/browser/debugHover.ts @@ -14,7 +14,7 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { IContentWidget, ICodeEditor, IContentWidgetPosition, ContentWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IDebugService, IExpression, IExpressionContainer } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, IExpression, IExpressionContainer, IStackFrame } from 'vs/workbench/contrib/debug/common/debug'; import { Expression } from 'vs/workbench/contrib/debug/common/debugModel'; import { renderExpressionValue, replaceWhitespace } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; @@ -34,6 +34,34 @@ import { VariablesRenderer } from 'vs/workbench/contrib/debug/browser/variablesV const $ = dom.$; const MAX_TREE_HEIGHT = 324; +async function doFindExpression(container: IExpressionContainer, namesToFind: string[]): Promise { + if (!container) { + return Promise.resolve(null); + } + + const children = await container.getChildren(); + // look for our variable in the list. First find the parents of the hovered variable if there are any. + const filtered = children.filter(v => namesToFind[0] === v.name); + if (filtered.length !== 1) { + return null; + } + + if (namesToFind.length === 1) { + return filtered[0]; + } else { + return doFindExpression(filtered[0], namesToFind.slice(1)); + } +} + +export async function findExpressionInStackFrame(stackFrame: IStackFrame, namesToFind: string[]): Promise { + const scopes = await stackFrame.getScopes(); + const nonExpensive = scopes.filter(s => !s.expensive); + const expressions = coalesce(await Promise.all(nonExpensive.map(scope => doFindExpression(scope, namesToFind)))); + + // only show if all expressions found have the same value + return expressions.length > 0 && expressions.every(e => e.value === expressions[0].value) ? expressions[0] : undefined; +} + export class DebugHoverWidget implements IContentWidget { static readonly ID = 'debug.hoverWidget'; @@ -107,7 +135,7 @@ export class DebugHoverWidget implements IContentWidget { if (colors.editorHoverForeground) { this.domNode.style.color = colors.editorHoverForeground.toString(); } else { - this.domNode.style.color = null; + this.domNode.style.color = ''; } })); this.toDispose.push(this.tree.onDidChangeContentHeight(() => this.layoutTreeAndContainer())); @@ -166,7 +194,10 @@ export class DebugHoverWidget implements IContentWidget { expression = new Expression(matchingExpression); await expression.evaluate(session, this.debugService.getViewModel().focusedStackFrame, 'hover'); } else { - expression = await this.findExpressionInStackFrame(coalesce(matchingExpression.split('.').map(word => word.trim()))); + const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; + if (focusedStackFrame) { + expression = await findExpressionInStackFrame(focusedStackFrame, coalesce(matchingExpression.split('.').map(word => word.trim()))); + } } if (!expression || (expression instanceof Expression && !expression.available)) { @@ -186,38 +217,6 @@ export class DebugHoverWidget implements IContentWidget { className: 'hoverHighlight' }); - private async doFindExpression(container: IExpressionContainer, namesToFind: string[]): Promise { - if (!container) { - return Promise.resolve(null); - } - - const children = await container.getChildren(); - // look for our variable in the list. First find the parents of the hovered variable if there are any. - const filtered = children.filter(v => namesToFind[0] === v.name); - if (filtered.length !== 1) { - return null; - } - - if (namesToFind.length === 1) { - return filtered[0]; - } else { - return this.doFindExpression(filtered[0], namesToFind.slice(1)); - } - } - - private async findExpressionInStackFrame(namesToFind: string[]): Promise { - const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; - if (!focusedStackFrame) { - return undefined; - } - - const scopes = await focusedStackFrame.getScopes(); - const nonExpensive = scopes.filter(s => !s.expensive); - const expressions = coalesce(await Promise.all(nonExpensive.map(scope => this.doFindExpression(scope, namesToFind)))); - // only show if all expressions found have the same value - return expressions.length > 0 && expressions.every(e => e.value === expressions[0].value) ? expressions[0] : undefined; - } - private async doShow(position: Position, expression: IExpression, focus: boolean, forceValueHover = false): Promise { if (!this.domNode) { this.create(); diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 27024734c8..1a0d643eae 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -36,7 +36,7 @@ import { IAction } from 'vs/base/common/actions'; import { deepClone, equals } from 'vs/base/common/objects'; import { DebugSession } from 'vs/workbench/contrib/debug/browser/debugSession'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { IDebugService, State, IDebugSession, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_STATE, CONTEXT_IN_DEBUG_MODE, IThread, IDebugConfiguration, VIEWLET_ID, REPL_ID, IConfig, ILaunch, IViewModel, IConfigurationManager, IDebugModel, IEnablement, IBreakpoint, IBreakpointData, ICompound, IGlobalConfig, IStackFrame, AdapterEndEvent, getStateLabel, IDebugSessionOptions, CONTEXT_DEBUG_UX } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, State, IDebugSession, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_STATE, CONTEXT_IN_DEBUG_MODE, IThread, IDebugConfiguration, VIEWLET_ID, DEBUG_PANEL_ID, IConfig, ILaunch, IViewModel, IConfigurationManager, IDebugModel, IEnablement, IBreakpoint, IBreakpointData, ICompound, IGlobalConfig, IStackFrame, AdapterEndEvent, getStateLabel, IDebugSessionOptions, CONTEXT_DEBUG_UX, REPL_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; import { getExtensionHostDebugSession } from 'vs/workbench/contrib/debug/common/debugUtils'; import { isErrorWithActions } from 'vs/base/common/errorsWithActions'; import { RunOnceScheduler } from 'vs/base/common/async'; @@ -45,6 +45,7 @@ import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { TaskRunResult, DebugTaskRunner } from 'vs/workbench/contrib/debug/browser/debugTaskRunner'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; +import { IViewsService } from 'vs/workbench/common/views'; const DEBUG_BREAKPOINTS_KEY = 'debug.breakpoint'; const DEBUG_FUNCTION_BREAKPOINTS_KEY = 'debug.functionbreakpoint'; @@ -80,6 +81,7 @@ export class DebugService implements IDebugService { @ITextFileService private readonly textFileService: ITextFileService, @IViewletService private readonly viewletService: IViewletService, @IPanelService private readonly panelService: IPanelService, + @IViewsService private readonly viewsService: IViewsService, @INotificationService private readonly notificationService: INotificationService, @IDialogService private readonly dialogService: IDialogService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @@ -290,7 +292,7 @@ export class DebugService implements IDebugService { "Compound must have \"configurations\" attribute set in order to start multiple configurations.")); } if (compound.preLaunchTask) { - const taskResult = await this.taskRunner.runTaskAndCheckErrors(launch?.workspace || this.contextService.getWorkspace(), compound.preLaunchTask, this.showError); + const taskResult = await this.taskRunner.runTaskAndCheckErrors(launch?.workspace || this.contextService.getWorkspace(), compound.preLaunchTask, (msg, actions) => this.showError(msg, actions)); if (taskResult === TaskRunResult.Failure) { this.endInitializingState(); return false; @@ -379,13 +381,26 @@ export class DebugService implements IDebugService { // a falsy config indicates an aborted launch if (configByProviders && configByProviders.type) { try { - const resolvedConfig = await this.substituteVariables(launch, configByProviders); - + let resolvedConfig = await this.substituteVariables(launch, configByProviders); if (!resolvedConfig) { - // User canceled resolving of interactive variables, silently return + // User cancelled resolving of interactive variables, silently return return false; } + if (!this.initCancellationToken) { + // User cancelled, silently return + return false; + } + + const cfg = await this.configurationManager.resolveDebugConfigurationWithSubstitutedVariables(launch && launch.workspace ? launch.workspace.uri : undefined, type, resolvedConfig, this.initCancellationToken.token); + if (!cfg) { + if (launch && type && cfg === null && this.initCancellationToken) { // show launch.json only for "config" being "null". + await launch.openConfigFile(false, true, type, this.initCancellationToken.token); + } + return false; + } + resolvedConfig = cfg; + if (!this.configurationManager.getDebugger(resolvedConfig.type) || (configByProviders.request !== 'attach' && configByProviders.request !== 'launch')) { let message: string; if (configByProviders.request !== 'attach' && configByProviders.request !== 'launch') { @@ -401,8 +416,8 @@ export class DebugService implements IDebugService { return false; } - const workspace = launch ? launch.workspace : this.contextService.getWorkspace(); - const taskResult = await this.taskRunner.runTaskAndCheckErrors(workspace, resolvedConfig.preLaunchTask, this.showError); + const workspace = launch?.workspace || this.contextService.getWorkspace(); + const taskResult = await this.taskRunner.runTaskAndCheckErrors(workspace, resolvedConfig.preLaunchTask, (msg, actions) => this.showError(msg, actions)); if (taskResult === TaskRunResult.Success) { return this.doCreateSession(launch?.workspace, { resolved: resolvedConfig, unresolved: unresolvedConfig }, options); } @@ -413,16 +428,16 @@ export class DebugService implements IDebugService { } else if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { await this.showError(nls.localize('noFolderWorkspaceDebugError', "The active file can not be debugged. Make sure it is saved and that you have a debug extension installed for that file type.")); } - if (launch) { - await launch.openConfigFile(false, true, undefined, this.initCancellationToken ? this.initCancellationToken.token : undefined); + if (launch && this.initCancellationToken) { + await launch.openConfigFile(false, true, undefined, this.initCancellationToken.token); } return false; } } - if (launch && type && configByProviders === null) { // show launch.json only for "config" being "null". - await launch.openConfigFile(false, true, type, this.initCancellationToken ? this.initCancellationToken.token : undefined); + if (launch && type && configByProviders === null && this.initCancellationToken) { // show launch.json only for "config" being "null". + await launch.openConfigFile(false, true, type, this.initCancellationToken.token); } return false; @@ -453,7 +468,7 @@ export class DebugService implements IDebugService { const internalConsoleOptions = session.configuration.internalConsoleOptions || this.configurationService.getValue('debug').internalConsoleOptions; if (internalConsoleOptions === 'openOnSessionStart' || (this.viewModel.firstSessionStart && internalConsoleOptions === 'openOnFirstSessionStart')) { - this.panelService.openPanel(REPL_ID, false); + this.viewsService.openView(REPL_VIEW_ID, false); } this.viewModel.firstSessionStart = false; @@ -479,7 +494,7 @@ export class DebugService implements IDebugService { // Show the repl if some error got logged there #5870 if (session && session.getReplElements().length > 0) { - this.panelService.openPanel(REPL_ID, false); + this.viewsService.openView(REPL_VIEW_ID, false); } if (session.configuration && session.configuration.request === 'attach' && session.configuration.__autoAttach) { @@ -563,8 +578,11 @@ export class DebugService implements IDebugService { // Data breakpoints that can not be persisted should be cleared when a session ends const dataBreakpoints = this.model.getDataBreakpoints().filter(dbp => !dbp.canPersist); dataBreakpoints.forEach(dbp => this.model.removeDataBreakpoints(dbp.getId())); - } + if (this.panelService.getLastActivePanelId() === DEBUG_PANEL_ID && this.configurationService.getValue('debug').console.closeOnEnd) { + this.panelService.hideActivePanel(); + } + } })); } @@ -579,7 +597,7 @@ export class DebugService implements IDebugService { } await this.taskRunner.runTask(session.root, session.configuration.postDebugTask); - return this.taskRunner.runTaskAndCheckErrors(session.root, session.configuration.preLaunchTask, this.showError); + return this.taskRunner.runTaskAndCheckErrors(session.root, session.configuration.preLaunchTask, (msg, actions) => this.showError(msg, actions)); }; const extensionDebugSession = getExtensionHostDebugSession(session); @@ -636,6 +654,9 @@ export class DebugService implements IDebugService { const resolvedByProviders = await this.configurationManager.resolveConfigurationByProviders(launch.workspace ? launch.workspace.uri : undefined, unresolved.type, unresolved, this.initCancellationToken.token); if (resolvedByProviders) { resolved = await this.substituteVariables(launch, resolvedByProviders); + if (resolved && this.initCancellationToken) { + resolved = await this.configurationManager.resolveDebugConfigurationWithSubstitutedVariables(launch && launch.workspace ? launch.workspace.uri : undefined, unresolved.type, resolved, this.initCancellationToken.token); + } } else { resolved = resolvedByProviders; } @@ -660,13 +681,13 @@ export class DebugService implements IDebugService { } stopSession(session: IDebugSession): Promise { - if (session) { return session.terminate(); } const sessions = this.model.getSessions(); if (sessions.length === 0) { + this.taskRunner.cancel(); this.endInitializingState(); } @@ -708,33 +729,8 @@ export class DebugService implements IDebugService { //---- focus management - async focusStackFrame(stackFrame: IStackFrame | undefined, thread?: IThread, session?: IDebugSession, explicit?: boolean): Promise { - if (!session) { - if (stackFrame || thread) { - session = stackFrame ? stackFrame.thread.session : thread!.session; - } else { - const sessions = this.model.getSessions(); - const stoppedSession = sessions.filter(s => s.state === State.Stopped).shift(); - session = stoppedSession || (sessions.length ? sessions[0] : undefined); - } - } - - if (!thread) { - if (stackFrame) { - thread = stackFrame.thread; - } else { - const threads = session ? session.getAllThreads() : undefined; - const stoppedThread = threads && threads.filter(t => t.stopped).shift(); - thread = stoppedThread || (threads && threads.length ? threads[0] : undefined); - } - } - - if (!stackFrame) { - if (thread) { - const callStack = thread.getCallStack(); - stackFrame = first(callStack, sf => !!(sf && sf.source && sf.source.available && sf.source.presentationHint !== 'deemphasize'), undefined); - } - } + async focusStackFrame(_stackFrame: IStackFrame | undefined, _thread?: IThread, _session?: IDebugSession, explicit?: boolean): Promise { + const { stackFrame, thread, session } = getStackFrameThreadAndSessionToFocus(this.model, _stackFrame, _thread, _session); if (stackFrame) { const editor = await stackFrame.openInEditor(this.editorService, true); @@ -760,9 +756,11 @@ export class DebugService implements IDebugService { //---- watches - addWatchExpression(name: string): void { + addWatchExpression(name?: string): void { const we = this.model.addWatchExpression(name); - this.viewModel.setSelectedExpression(we); + if (!name) { + this.viewModel.setSelectedExpression(we); + } this.storeWatchExpressions(); } @@ -1096,3 +1094,34 @@ export class DebugService implements IDebugService { }); } } + +export function getStackFrameThreadAndSessionToFocus(model: IDebugModel, stackFrame: IStackFrame | undefined, thread?: IThread, session?: IDebugSession): { stackFrame: IStackFrame | undefined, thread: IThread | undefined, session: IDebugSession | undefined } { + if (!session) { + if (stackFrame || thread) { + session = stackFrame ? stackFrame.thread.session : thread!.session; + } else { + const sessions = model.getSessions(); + const stoppedSession = sessions.filter(s => s.state === State.Stopped).shift(); + session = stoppedSession || (sessions.length ? sessions[0] : undefined); + } + } + + if (!thread) { + if (stackFrame) { + thread = stackFrame.thread; + } else { + const threads = session ? session.getAllThreads() : undefined; + const stoppedThread = threads && threads.filter(t => t.stopped).shift(); + thread = stoppedThread || (threads && threads.length ? threads[0] : undefined); + } + } + + if (!stackFrame) { + if (thread) { + const callStack = thread.getCallStack(); + stackFrame = first(callStack, sf => !!(sf && sf.source && sf.source.available && sf.source.presentationHint !== 'deemphasize'), undefined); + } + } + + return { session, thread, stackFrame }; +} diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 190fb399b6..cd2bbc9329 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -9,7 +9,6 @@ import * as nls from 'vs/nls'; import * as platform from 'vs/base/common/platform'; import severity from 'vs/base/common/severity'; import { Event, Emitter } from 'vs/base/common/event'; -import { CompletionItem, completionKindFromString } from 'vs/editor/common/modes'; import { Position, IPosition } from 'vs/editor/common/core/position'; import * as aria from 'vs/base/browser/ui/aria/aria'; import { IDebugSession, IConfig, IThread, IRawModelUpdate, IDebugService, IRawStoppedDetails, State, LoadedSourceEvent, IFunctionBreakpoint, IExceptionBreakpoint, IBreakpoint, IExceptionInfo, AdapterEndEvent, IDebugger, VIEWLET_ID, IDebugConfiguration, IReplElement, IStackFrame, IExpression, IReplElementSource, IDataBreakpoint, IDebugSessionOptions } from 'vs/workbench/contrib/debug/common/debug'; @@ -26,7 +25,6 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { normalizeDriveLetter } from 'vs/base/common/labels'; -import { Range } from 'vs/editor/common/core/range'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { ReplModel } from 'vs/workbench/contrib/debug/common/replModel'; @@ -34,6 +32,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { variableSetEmitter } from 'vs/workbench/contrib/debug/browser/variablesView'; import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; import { distinct } from 'vs/base/common/arrays'; +import { INotificationService } from 'vs/platform/notification/common/notification'; export class DebugSession implements IDebugSession { @@ -74,7 +73,8 @@ export class DebugSession implements IDebugSession { @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IProductService private readonly productService: IProductService, @IExtensionHostDebugService private readonly extensionHostDebugService: IExtensionHostDebugService, - @IOpenerService private readonly openerService: IOpenerService + @IOpenerService private readonly openerService: IOpenerService, + @INotificationService private readonly notificationService: INotificationService ) { this.id = generateUuid(); this._options = options || {}; @@ -565,35 +565,17 @@ export class DebugSession implements IDebugSession { } } - async completions(frameId: number | undefined, text: string, position: Position, overwriteBefore: number, token: CancellationToken): Promise { + async completions(frameId: number | undefined, text: string, position: Position, overwriteBefore: number, token: CancellationToken): Promise { if (!this.raw) { return Promise.reject(new Error('no debug adapter')); } - const response = await this.raw.completions({ + return this.raw.completions({ frameId, text, column: position.column, line: position.lineNumber, }, token); - - const result: CompletionItem[] = []; - if (response && response.body && response.body.targets) { - response.body.targets.forEach(item => { - if (item && item.label) { - result.push({ - label: item.label, - insertText: item.text || item.label, - kind: completionKindFromString(item.type || 'property'), - filterText: (item.start && item.length) ? text.substr(item.start, item.length).concat(item.label) : undefined, - range: Range.fromPositions(position.delta(0, -(item.length || overwriteBefore)), position), - sortText: item.sortText - }); - } - }); - } - - return result; } //---- threads @@ -711,6 +693,7 @@ export class DebugSession implements IDebugSession { await this.raw.configurationDone(); } catch (e) { // Disconnect the debug session on configuration done error #10596 + this.notificationService.error(e); if (this.raw) { this.raw.disconnect(); } @@ -921,6 +904,7 @@ export class DebugSession implements IDebugSession { this.raw.dispose(); } this.raw = undefined; + this.fetchThreadsScheduler = undefined; this.model.clearThreads(this.getId(), true); this._onDidChangeState.fire(); } diff --git a/src/vs/workbench/contrib/debug/browser/debugStatus.ts b/src/vs/workbench/contrib/debug/browser/debugStatus.ts index deb8112799..6cf5c911d5 100644 --- a/src/vs/workbench/contrib/debug/browser/debugStatus.ts +++ b/src/vs/workbench/contrib/debug/browser/debugStatus.ts @@ -10,7 +10,6 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IStatusbarEntry, IStatusbarService, StatusbarAlignment, IStatusbarEntryAccessor } from 'vs/workbench/services/statusbar/common/statusbar'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; - export class DebugStatusContribution implements IWorkbenchContribution { private showInStatusBar!: 'never' | 'always' | 'onFirstSessionStart'; @@ -56,20 +55,17 @@ export class DebugStatusContribution implements IWorkbenchContribution { })); } - private getText(): string { + private get entry(): IStatusbarEntry { + let text = ''; const manager = this.debugService.getConfigurationManager(); const name = manager.selectedConfiguration.name || ''; const nameAndLaunchPresent = name && manager.selectedConfiguration.launch; if (nameAndLaunchPresent) { - return '$(play) ' + (manager.getLaunches().length > 1 ? `${name} (${manager.selectedConfiguration.launch!.name})` : name); + text = '$(play) ' + (manager.getLaunches().length > 1 ? `${name} (${manager.selectedConfiguration.launch!.name})` : name); } - return ''; - } - - private get entry(): IStatusbarEntry { return { - text: this.getText(), + text: text, tooltip: nls.localize('selectAndStartDebug', "Select and start debug configuration"), command: 'workbench.action.debug.selectandstart' }; diff --git a/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts b/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts index 9864baa114..32f527dc15 100644 --- a/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts +++ b/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts @@ -38,6 +38,8 @@ export const enum TaskRunResult { export class DebugTaskRunner { + private canceled = false; + constructor( @ITaskService private readonly taskService: ITaskService, @IMarkerService private readonly markerService: IMarkerService, @@ -46,9 +48,17 @@ export class DebugTaskRunner { @IDialogService private readonly dialogService: IDialogService, ) { } + cancel(): void { + this.canceled = true; + } + async runTaskAndCheckErrors(root: IWorkspaceFolder | IWorkspace | undefined, taskId: string | TaskIdentifier | undefined, onError: (msg: string, actions: IAction[]) => Promise): Promise { try { + this.canceled = false; const taskSummary = await this.runTask(root, taskId); + if (this.canceled) { + return TaskRunResult.Failure; + } const errorCount = taskId ? this.markerService.getStatistics().errors : 0; const successExitCode = taskSummary && taskSummary.exitCode === 0; diff --git a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts index db8d0ec28a..da0931e840 100644 --- a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts +++ b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts @@ -36,78 +36,6 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; const DEBUG_TOOLBAR_POSITION_KEY = 'debug.actionswidgetposition'; const DEBUG_TOOLBAR_Y_KEY = 'debug.actionswidgety'; -export const debugToolBarBackground = registerColor('debugToolBar.background', { - dark: '#333333', - light: '#F3F3F3', - hc: '#000000' -}, localize('debugToolBarBackground', "Debug toolbar background color.")); - -export const debugToolBarBorder = registerColor('debugToolBar.border', { - dark: null, - light: null, - hc: null -}, localize('debugToolBarBorder', "Debug toolbar border color.")); - -export const debugIconStartForeground = registerColor('debugIcon.startForeground', { - dark: '#89D185', - light: '#388A34', - hc: '#89D185' -}, localize('debugIcon.startForeground', "Debug toolbar icon for start debugging.")); - -export const debugIconPauseForeground = registerColor('debugIcon.pauseForeground', { - dark: '#75BEFF', - light: '#007ACC', - hc: '#75BEFF' -}, localize('debugIcon.pauseForeground', "Debug toolbar icon for pause.")); - -export const debugIconStopForeground = registerColor('debugIcon.stopForeground', { - dark: '#F48771', - light: '#A1260D', - hc: '#F48771' -}, localize('debugIcon.stopForeground', "Debug toolbar icon for stop.")); - -export const debugIconDisconnectForeground = registerColor('debugIcon.disconnectForeground', { - dark: '#F48771', - light: '#A1260D', - hc: '#F48771' -}, localize('debugIcon.disconnectForeground', "Debug toolbar icon for disconnect.")); - -export const debugIconRestartForeground = registerColor('debugIcon.restartForeground', { - dark: '#89D185', - light: '#388A34', - hc: '#89D185' -}, localize('debugIcon.restartForeground', "Debug toolbar icon for restart.")); - -export const debugIconStepOverForeground = registerColor('debugIcon.stepOverForeground', { - dark: '#75BEFF', - light: '#007ACC', - hc: '#75BEFF' -}, localize('debugIcon.stepOverForeground', "Debug toolbar icon for step over.")); - -export const debugIconStepIntoForeground = registerColor('debugIcon.stepIntoForeground', { - dark: '#75BEFF', - light: '#007ACC', - hc: '#75BEFF' -}, localize('debugIcon.stepIntoForeground', "Debug toolbar icon for step into.")); - -export const debugIconStepOutForeground = registerColor('debugIcon.stepOutForeground', { - dark: '#75BEFF', - light: '#007ACC', - hc: '#75BEFF' -}, localize('debugIcon.stepOutForeground', "Debug toolbar icon for step over.")); - -export const debugIconContinueForeground = registerColor('debugIcon.continueForeground', { - dark: '#75BEFF', - light: '#007ACC', - hc: '#75BEFF' -}, localize('debugIcon.continueForeground', "Debug toolbar icon for continue.")); - -export const debugIconStepBackForeground = registerColor('debugIcon.stepBackForeground', { - dark: '#75BEFF', - light: '#007ACC', - hc: '#75BEFF' -}, localize('debugIcon.stepBackForeground', "Debug toolbar icon for step back.")); - export class DebugToolBar extends Themable implements IWorkbenchContribution { private $el: HTMLElement; @@ -184,9 +112,7 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { }, 20)); this.updateStyles(); - this.registerListeners(); - this.hide(); } @@ -327,7 +253,7 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { dom.hide(this.$el); } - public static getActions(menu: IMenu, debugService: IDebugService, instantiationService: IInstantiationService): { actions: IAction[], disposable: IDisposable } { + static getActions(menu: IMenu, debugService: IDebugService, instantiationService: IInstantiationService): { actions: IAction[], disposable: IDisposable } { const actions: IAction[] = []; const disposable = createAndFillInActionBarActions(menu, undefined, actions, () => false); if (debugService.getViewModel().isMultiSessionView()) { @@ -340,7 +266,7 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { }; } - public dispose(): void { + dispose(): void { super.dispose(); if (this.$el) { @@ -353,6 +279,78 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { } } +export const debugToolBarBackground = registerColor('debugToolBar.background', { + dark: '#333333', + light: '#F3F3F3', + hc: '#000000' +}, localize('debugToolBarBackground', "Debug toolbar background color.")); + +export const debugToolBarBorder = registerColor('debugToolBar.border', { + dark: null, + light: null, + hc: null +}, localize('debugToolBarBorder', "Debug toolbar border color.")); + +export const debugIconStartForeground = registerColor('debugIcon.startForeground', { + dark: '#89D185', + light: '#388A34', + hc: '#89D185' +}, localize('debugIcon.startForeground', "Debug toolbar icon for start debugging.")); + +export const debugIconPauseForeground = registerColor('debugIcon.pauseForeground', { + dark: '#75BEFF', + light: '#007ACC', + hc: '#75BEFF' +}, localize('debugIcon.pauseForeground', "Debug toolbar icon for pause.")); + +export const debugIconStopForeground = registerColor('debugIcon.stopForeground', { + dark: '#F48771', + light: '#A1260D', + hc: '#F48771' +}, localize('debugIcon.stopForeground', "Debug toolbar icon for stop.")); + +export const debugIconDisconnectForeground = registerColor('debugIcon.disconnectForeground', { + dark: '#F48771', + light: '#A1260D', + hc: '#F48771' +}, localize('debugIcon.disconnectForeground', "Debug toolbar icon for disconnect.")); + +export const debugIconRestartForeground = registerColor('debugIcon.restartForeground', { + dark: '#89D185', + light: '#388A34', + hc: '#89D185' +}, localize('debugIcon.restartForeground', "Debug toolbar icon for restart.")); + +export const debugIconStepOverForeground = registerColor('debugIcon.stepOverForeground', { + dark: '#75BEFF', + light: '#007ACC', + hc: '#75BEFF' +}, localize('debugIcon.stepOverForeground', "Debug toolbar icon for step over.")); + +export const debugIconStepIntoForeground = registerColor('debugIcon.stepIntoForeground', { + dark: '#75BEFF', + light: '#007ACC', + hc: '#75BEFF' +}, localize('debugIcon.stepIntoForeground', "Debug toolbar icon for step into.")); + +export const debugIconStepOutForeground = registerColor('debugIcon.stepOutForeground', { + dark: '#75BEFF', + light: '#007ACC', + hc: '#75BEFF' +}, localize('debugIcon.stepOutForeground', "Debug toolbar icon for step over.")); + +export const debugIconContinueForeground = registerColor('debugIcon.continueForeground', { + dark: '#75BEFF', + light: '#007ACC', + hc: '#75BEFF' +}, localize('debugIcon.continueForeground', "Debug toolbar icon for continue.")); + +export const debugIconStepBackForeground = registerColor('debugIcon.stepBackForeground', { + dark: '#75BEFF', + light: '#007ACC', + hc: '#75BEFF' +}, localize('debugIcon.stepBackForeground', "Debug toolbar icon for step back.")); + registerThemingParticipant((theme, collector) => { const debugIconStartColor = theme.getColor(debugIconStartForeground); diff --git a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts index 48e864a687..a85fd1e758 100644 --- a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts +++ b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts @@ -8,7 +8,7 @@ import * as nls from 'vs/nls'; import { IAction } from 'vs/base/common/actions'; import * as DOM from 'vs/base/browser/dom'; import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; -import { IDebugService, VIEWLET_ID, State, BREAKPOINTS_VIEW_ID, IDebugConfiguration, REPL_ID, CONTEXT_DEBUG_UX, CONTEXT_DEBUG_UX_KEY } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, VIEWLET_ID, State, BREAKPOINTS_VIEW_ID, IDebugConfiguration, DEBUG_PANEL_ID, CONTEXT_DEBUG_UX, CONTEXT_DEBUG_UX_KEY } from 'vs/workbench/contrib/debug/common/debug'; import { StartAction, ConfigureAction, SelectAndStartAction, FocusSessionAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { StartDebugActionViewItem, FocusSessionActionViewItem } from 'vs/workbench/contrib/debug/browser/debugActionViewItems'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -105,8 +105,8 @@ export class DebugViewPaneContainer extends ViewPaneContainer { } @memoize - private get toggleReplAction(): ToggleReplAction { - return this._register(this.instantiationService.createInstance(ToggleReplAction, ToggleReplAction.ID, ToggleReplAction.LABEL)); + private get toggleReplAction(): OpenDebugPanelAction { + return this._register(this.instantiationService.createInstance(OpenDebugPanelAction, OpenDebugPanelAction.ID, OpenDebugPanelAction.LABEL)); } @memoize @@ -228,14 +228,16 @@ export class DebugViewPaneContainer extends ViewPaneContainer { } } -class ToggleReplAction extends TogglePanelAction { - static readonly ID = 'debug.toggleRepl'; - static readonly LABEL = nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'debugConsoleAction' }, 'Debug Console'); +export class OpenDebugPanelAction extends TogglePanelAction { + public static readonly ID = 'workbench.debug.action.toggleRepl'; + public static readonly LABEL = nls.localize('toggleDebugPanel', "Debug Console"); - constructor(id: string, label: string, - @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, - @IPanelService panelService: IPanelService + constructor( + id: string, + label: string, + @IPanelService panelService: IPanelService, + @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService ) { - super(id, label, REPL_ID, panelService, layoutService, 'debug-action codicon-terminal'); + super(id, label, DEBUG_PANEL_ID, panelService, layoutService); } } diff --git a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts index 3e151554f4..2a4476a09e 100644 --- a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts +++ b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts @@ -13,7 +13,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { renderViewTree } from 'vs/workbench/contrib/debug/browser/baseDebugView'; -import { IDebugSession, IDebugService, IDebugModel, CONTEXT_LOADED_SCRIPTS_ITEM_TYPE } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugSession, IDebugService, CONTEXT_LOADED_SCRIPTS_ITEM_TYPE } from 'vs/workbench/contrib/debug/common/debug'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -26,17 +26,19 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { ResourceLabels, IResourceLabelProps, IResourceLabelOptions, IResourceLabel } from 'vs/workbench/browser/labels'; import { FileKind } from 'vs/platform/files/common/files'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { ITreeRenderer, ITreeNode, ITreeFilter, TreeVisibility, TreeFilterResult, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; +import { ITreeNode, ITreeFilter, TreeVisibility, TreeFilterResult, ITreeElement } from 'vs/base/browser/ui/tree/tree'; import { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { WorkbenchAsyncDataTree, TreeResourceNavigator2 } from 'vs/platform/list/browser/listService'; +import { TreeResourceNavigator2, WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService'; import { dispose } from 'vs/base/common/lifecycle'; import { createMatches, FuzzyScore } from 'vs/base/common/filters'; import { DebugContentProvider } from 'vs/workbench/contrib/debug/common/debugContentProvider'; import { ILabelService } from 'vs/platform/label/common/label'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; +import type { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; +import type { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; -const SMART = true; +const NEW_STYLE_COMPRESS = true; // RFC 2396, Appendix A: https://www.ietf.org/rfc/rfc2396.txt const URI_SCHEMA_PATTERN = /^[a-zA-Z][a-zA-Z0-9\+\-\.]+:/; @@ -49,7 +51,7 @@ class BaseTreeItem { private _children = new Map(); private _source: Source | undefined; - constructor(private _parent: BaseTreeItem | undefined, private _label: string) { + constructor(private _parent: BaseTreeItem | undefined, private _label: string, public readonly isIncompressible = false) { this._showedMoreThanOne = false; } @@ -150,7 +152,7 @@ class BaseTreeItem { } // skips intermediate single-child nodes - getChildren(): Promise { + getChildren(): BaseTreeItem[] { const child = this.oneChild(); if (child) { return child.getChildren(); @@ -159,7 +161,7 @@ class BaseTreeItem { for (let child of this._children.values()) { array.push(child); } - return Promise.resolve(array.sort((a, b) => this.compare(a, b))); + return array.sort((a, b) => this.compare(a, b)); } // skips intermediate single-child nodes @@ -205,7 +207,7 @@ class BaseTreeItem { } private oneChild(): BaseTreeItem | undefined { - if (SMART && !this._source && !this._showedMoreThanOne && !(this instanceof RootFolderTreeItem) && !(this instanceof SessionTreeItem)) { + if (!this._source && !this._showedMoreThanOne && this.skipOneChild()) { if (this._children.size === 1) { return this._children.values().next().value; } @@ -216,22 +218,28 @@ class BaseTreeItem { } return undefined; } + + private skipOneChild(): boolean { + if (NEW_STYLE_COMPRESS) { + // if the root node has only one Session, don't show the session + return this instanceof RootTreeItem; + } else { + return !(this instanceof RootFolderTreeItem) && !(this instanceof SessionTreeItem); + } + } } class RootFolderTreeItem extends BaseTreeItem { constructor(parent: BaseTreeItem, public folder: IWorkspaceFolder) { - super(parent, folder.name); + super(parent, folder.name, true); } } class RootTreeItem extends BaseTreeItem { - constructor(private _debugModel: IDebugModel, private _environmentService: IEnvironmentService, private _contextService: IWorkspaceContextService, private _labelService: ILabelService) { + constructor(private _environmentService: IEnvironmentService, private _contextService: IWorkspaceContextService, private _labelService: ILabelService) { super(undefined, 'Root'); - this._debugModel.getSessions().forEach(session => { - this.add(session); - }); } add(session: IDebugSession): SessionTreeItem { @@ -248,14 +256,12 @@ class SessionTreeItem extends BaseTreeItem { private static readonly URL_REGEXP = /^(https?:\/\/[^/]+)(\/.*)$/; private _session: IDebugSession; - private _initialized: boolean; private _map = new Map(); private _labelService: ILabelService; constructor(labelService: ILabelService, parent: BaseTreeItem, session: IDebugSession, private _environmentService: IEnvironmentService, private rootProvider: IWorkspaceContextService) { - super(parent, session.getLabel()); + super(parent, session.getLabel(), true); this._labelService = labelService; - this._initialized = false; this._session = session; } @@ -275,19 +281,6 @@ class SessionTreeItem extends BaseTreeItem { return true; } - getChildren(): Promise { - - if (!this._initialized) { - this._initialized = true; - return this._session.getLoadedSources().then(paths => { - paths.forEach(path => this.addPath(path)); - return super.getChildren(); - }); - } - - return super.getChildren(); - } - protected compare(a: BaseTreeItem, b: BaseTreeItem): number { const acat = this.category(a); const bcat = this.category(b); @@ -388,11 +381,30 @@ class SessionTreeItem extends BaseTreeItem { } } +interface IViewState { + readonly expanded: Set; +} + +/** + * This maps a model item into a view model item. + */ +function asTreeElement(item: BaseTreeItem, viewState?: IViewState): ITreeElement { + const children = item.getChildren(); + const collapsed = viewState ? !viewState.expanded.has(item.getId()) : !(item instanceof SessionTreeItem); + + return { + element: item, + collapsed, + collapsible: item.hasChildren(), + children: children.map(i => asTreeElement(i, viewState)) + }; +} + export class LoadedScriptsView extends ViewPane { private treeContainer!: HTMLElement; private loadedScriptsItemType: IContextKey; - private tree!: WorkbenchAsyncDataTree; + private tree!: WorkbenchCompressibleObjectTree; private treeLabels!: ResourceLabels; private changeScheduler!: RunOnceScheduler; private treeNeedsRefreshOnVisible = false; @@ -402,7 +414,7 @@ export class LoadedScriptsView extends ViewPane { options: IViewletViewOptions, @IContextMenuService contextMenuService: IContextMenuService, @IKeybindingService keybindingService: IKeybindingService, - @IInstantiationService private readonly instantiationService: IInstantiationService, + @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, @IEditorService private readonly editorService: IEditorService, @IContextKeyService readonly contextKeyService: IContextKeyService, @@ -411,7 +423,7 @@ export class LoadedScriptsView extends ViewPane { @IDebugService private readonly debugService: IDebugService, @ILabelService private readonly labelService: ILabelService ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('loadedScriptsSection', "Loaded Scripts Section") }, keybindingService, contextMenuService, configurationService, contextKeyService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('loadedScriptsSection', "Loaded Scripts Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, instantiationService); this.loadedScriptsItemType = CONTEXT_LOADED_SCRIPTS_ITEM_TYPE.bindTo(contextKeyService); } @@ -423,20 +435,30 @@ export class LoadedScriptsView extends ViewPane { this.filter = new LoadedScriptsFilter(); - const root = new RootTreeItem(this.debugService.getModel(), this.environmentService, this.contextService, this.labelService); + const root = new RootTreeItem(this.environmentService, this.contextService, this.labelService); this.treeLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility }); this._register(this.treeLabels); - this.tree = this.instantiationService.createInstance>(WorkbenchAsyncDataTree, 'LoadedScriptsView', this.treeContainer, new LoadedScriptsDelegate(), + this.tree = this.instantiationService.createInstance(WorkbenchCompressibleObjectTree, + 'LoadedScriptsView', + this.treeContainer, + new LoadedScriptsDelegate(), [new LoadedScriptsRenderer(this.treeLabels)], - new LoadedScriptsDataSource(), { + compressionEnabled: NEW_STYLE_COMPRESS, + collapseByDefault: true, + hideTwistiesOfChildlessElements: true, identityProvider: { getId: (element: LoadedScriptsItem) => element.getId() }, keyboardNavigationLabelProvider: { - getKeyboardNavigationLabel: (element: LoadedScriptsItem) => element.getLabel() + getKeyboardNavigationLabel: (element: LoadedScriptsItem) => { + return element.getLabel(); + }, + getCompressedNodeKeyboardNavigationLabel: (elements: LoadedScriptsItem[]) => { + return elements.map(e => e.getLabel()).join('/'); + } }, filter: this.filter, accessibilityProvider: new LoadedSciptsAccessibilityProvider(), @@ -447,12 +469,14 @@ export class LoadedScriptsView extends ViewPane { } ); - this.tree.setInput(root); + const updateView = (viewState?: IViewState) => this.tree.setChildren(null, asTreeElement(root, viewState).children); + + updateView(); this.changeScheduler = new RunOnceScheduler(() => { this.treeNeedsRefreshOnVisible = false; if (this.tree) { - this.tree.updateChildren(); + updateView(); } }, 300); this._register(this.changeScheduler); @@ -486,12 +510,19 @@ export class LoadedScriptsView extends ViewPane { } }; + const addSourcePathsToSession = (session: IDebugSession) => { + const sessionNode = root.add(session); + return session.getLoadedSources().then(paths => { + paths.forEach(path => sessionNode.addPath(path)); + scheduleRefreshOnVisible(); + }); + }; + const registerSessionListeners = (session: IDebugSession) => { this._register(session.onDidChangeName(() => { // Re-add session, this will trigger proper sorting and id recalculation. root.remove(session.getId()); - root.add(session); - scheduleRefreshOnVisible(); + addSourcePathsToSession(session); })); this._register(session.onDidLoadedSource(event => { let sessionRoot: SessionTreeItem; @@ -534,6 +565,38 @@ export class LoadedScriptsView extends ViewPane { this.changeScheduler.schedule(); } })); + + // feature: expand all nodes when filtering (not when finding) + let viewState: IViewState | undefined; + this._register(this.tree.onDidChangeTypeFilterPattern(pattern => { + if (!this.tree.options.filterOnType) { + return; + } + + if (!viewState && pattern) { + const expanded = new Set(); + const visit = (node: ITreeNode) => { + if (node.element && !node.collapsed) { + expanded.add(node.element.getId()); + } + + for (const child of node.children) { + visit(child); + } + }; + + visit(this.tree.getNode()); + viewState = { expanded }; + this.tree.expandAll(); + } else if (!pattern && viewState) { + this.tree.setFocus([]); + updateView(viewState); + viewState = undefined; + } + })); + + // populate tree model with source paths from all debug sessions + this.debugService.getModel().getSessions().forEach(session => addSourcePathsToSession(session)); } layoutBody(height: number, width: number): void { @@ -558,22 +621,11 @@ class LoadedScriptsDelegate implements IListVirtualDelegate { } } -class LoadedScriptsDataSource implements IAsyncDataSource { - - hasChildren(element: LoadedScriptsItem): boolean { - return element.hasChildren(); - } - - getChildren(element: LoadedScriptsItem): Promise { - return element.getChildren(); - } -} - interface ILoadedScriptsItemTemplateData { label: IResourceLabel; } -class LoadedScriptsRenderer implements ITreeRenderer { +class LoadedScriptsRenderer implements ICompressibleTreeRenderer { static readonly ID = 'lsrenderer'; @@ -594,9 +646,23 @@ class LoadedScriptsRenderer implements ITreeRenderer, index: number, data: ILoadedScriptsItemTemplateData): void { const element = node.element; + const label = element.getLabel(); + + this.render(element, label, data, node.filterData); + } + + renderCompressedElements(node: ITreeNode, FuzzyScore>, index: number, data: ILoadedScriptsItemTemplateData, height: number | undefined): void { + + const element = node.element.elements[node.element.elements.length - 1]; + const labels = node.element.elements.map(e => e.getLabel()); + + this.render(element, labels, data, node.filterData); + } + + private render(element: BaseTreeItem, labels: string | string[], data: ILoadedScriptsItemTemplateData, filterData: FuzzyScore | undefined) { const label: IResourceLabelProps = { - name: element.getLabel() + name: labels }; const options: IResourceLabelOptions = { title: element.getHoverLabel() @@ -621,7 +687,7 @@ class LoadedScriptsRenderer implements ITreeRenderer .noworkspace-view { - padding: 0 20px 0 20px; -} - -.debug-viewlet > .noworkspace-view > p { - line-height: 1.5em; -} diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index b472fdcfdf..bece00f46f 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -12,22 +12,21 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; import { ITextModel } from 'vs/editor/common/model'; +import { Range } from 'vs/editor/common/core/range'; import { Position } from 'vs/editor/common/core/position'; import { registerEditorAction, ServicesAccessor, EditorAction } from 'vs/editor/browser/editorExtensions'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { Panel } from 'vs/workbench/browser/panel'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { memoize } from 'vs/base/common/decorators'; import { dispose, IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; -import { IDebugService, REPL_ID, DEBUG_SCHEME, CONTEXT_IN_DEBUG_REPL, IDebugSession, State, IReplElement, IDebugConfiguration } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, DEBUG_SCHEME, CONTEXT_IN_DEBUG_REPL, IDebugSession, State, IReplElement, IDebugConfiguration, REPL_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; import { HistoryNavigator } from 'vs/base/common/history'; import { IHistoryNavigationWidget } from 'vs/base/browser/history'; import { createAndBindHistoryNavigationWidgetScopedContextKeyService } from 'vs/platform/browser/contextScopedHistoryWidget'; @@ -37,9 +36,8 @@ import { IDecorationOptions } from 'vs/editor/common/editorCommon'; import { transparent, editorForeground } from 'vs/platform/theme/common/colorRegistry'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { FocusSessionActionViewItem } from 'vs/workbench/contrib/debug/browser/debugActionViewItems'; -import { CompletionContext, CompletionList, CompletionProviderRegistry } from 'vs/editor/common/modes'; +import { CompletionContext, CompletionList, CompletionProviderRegistry, CompletionItem, completionKindFromString, CompletionItemKind } from 'vs/editor/common/modes'; import { first } from 'vs/base/common/arrays'; -import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { ITreeNode, ITreeContextMenuEvent, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; @@ -55,6 +53,9 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService import { PANEL_BACKGROUND } from 'vs/workbench/common/theme'; import { ReplDelegate, ReplVariablesRenderer, ReplSimpleElementsRenderer, ReplEvaluationInputsRenderer, ReplEvaluationResultsRenderer, ReplRawObjectsRenderer, ReplDataSource, ReplAccessibilityProvider } from 'vs/workbench/contrib/debug/browser/replViewer'; import { localize } from 'vs/nls'; +import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IViewsService } from 'vs/workbench/common/views'; const $ = dom.$; @@ -76,7 +77,8 @@ function revealLastElement(tree: WorkbenchAsyncDataTree) { } const sessionsToIgnore = new Set(); -export class Repl extends Panel implements IPrivateReplService, IHistoryNavigationWidget { + +export class Repl extends ViewPane implements IPrivateReplService, IHistoryNavigationWidget { _serviceBrand: undefined; private static readonly REFRESH_DELAY = 100; // delay in ms to refresh the repl for new elements to show @@ -99,21 +101,22 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati private modelChangeListener: IDisposable = Disposable.None; constructor( + options: IViewPaneOptions, @IDebugService private readonly debugService: IDebugService, - @ITelemetryService telemetryService: ITelemetryService, - @IInstantiationService private readonly instantiationService: IInstantiationService, + @IInstantiationService instantiationService: IInstantiationService, @IStorageService private readonly storageService: IStorageService, @IThemeService protected themeService: IThemeService, @IModelService private readonly modelService: IModelService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @ICodeEditorService codeEditorService: ICodeEditorService, - @IContextMenuService private readonly contextMenuService: IContextMenuService, - @IConfigurationService private readonly configurationService: IConfigurationService, + @IContextMenuService contextMenuService: IContextMenuService, + @IConfigurationService configurationService: IConfigurationService, @ITextResourcePropertiesService private readonly textResourcePropertiesService: ITextResourcePropertiesService, @IClipboardService private readonly clipboardService: IClipboardService, - @IEditorService private readonly editorService: IEditorService + @IEditorService private readonly editorService: IEditorService, + @IKeybindingService keybindingService: IKeybindingService ) { - super(REPL_ID, telemetryService, themeService, storageService); + super({ ...(options as IViewPaneOptions), id: REPL_VIEW_ID, ariaHeaderLabel: localize('debugConsole', "Debug Console") }, keybindingService, contextMenuService, configurationService, contextKeyService, instantiationService); this.history = new HistoryNavigator(JSON.parse(this.storageService.get(HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]')), 50); codeEditorService.registerDecorationType(DECORATION_KEY, {}); @@ -141,7 +144,34 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati const text = model.getValue(); const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; const frameId = focusedStackFrame ? focusedStackFrame.frameId : undefined; - const suggestions = await session.completions(frameId, text, position, overwriteBefore, token); + const response = await session.completions(frameId, text, position, overwriteBefore, token); + + const suggestions: CompletionItem[] = []; + const computeRange = (length: number) => Range.fromPositions(position.delta(0, -length), position); + if (response && response.body && response.body.targets) { + response.body.targets.forEach(item => { + if (item && item.label) { + suggestions.push({ + label: item.label, + insertText: item.text || item.label, + kind: completionKindFromString(item.type || 'property'), + filterText: (item.start && item.length) ? text.substr(item.start, item.length).concat(item.label) : undefined, + range: computeRange(item.length || overwriteBefore), + sortText: item.sortText + }); + } + }); + } + + const history = this.history.getHistory(); + history.forEach(h => suggestions.push({ + label: h, + insertText: h, + kind: CompletionItemKind.Text, + range: computeRange(h.length), + sortText: 'ZZZ' + })); + return { suggestions }; } @@ -159,7 +189,7 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati if (!input || input.state === State.Inactive) { await this.selectSession(newSession); } - this.updateTitleArea(); + this.updateActions(); })); this._register(this.themeService.onThemeChange(() => { this.refreshReplElements(false); @@ -167,7 +197,7 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati this.updateInputDecoration(); } })); - this._register(this.onDidChangeVisibility(visible => { + this._register(this.onDidChangeBodyVisibility(visible => { if (!visible) { dispose(this.model); } else { @@ -300,7 +330,7 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati // Ignore inactive sessions which got cleared - so they are not shown any more sessionsToIgnore.add(session); await this.selectSession(); - this.updateTitleArea(); + this.updateActions(); } } this.replInput.focus(); @@ -317,7 +347,7 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati this.replInputLineCount = 1; if (shouldRelayout) { // Trigger a layout to shrink a potential multi line input - this.layout(this.dimension); + this.layoutBody(this.dimension.height, this.dimension.width); } } } @@ -338,21 +368,21 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati return removeAnsiEscapeCodes(text); } - layout(dimension: dom.Dimension): void { - this.dimension = dimension; + protected layoutBody(height: number, width: number): void { + this.dimension = new dom.Dimension(width, height); const replInputHeight = Repl.REPL_INPUT_LINE_HEIGHT * this.replInputLineCount; if (this.tree) { const lastElementVisible = this.tree.scrollTop + this.tree.renderHeight >= this.tree.scrollHeight; - const treeHeight = dimension.height - replInputHeight; + const treeHeight = height - replInputHeight; this.tree.getHTMLElement().style.height = `${treeHeight}px`; - this.tree.layout(treeHeight, dimension.width); + this.tree.layout(treeHeight, width); if (lastElementVisible) { revealLastElement(this.tree); } } this.replInputContainer.style.height = `${replInputHeight}px`; - this.replInput.layout({ width: dimension.width - 20, height: replInputHeight }); + this.replInput.layout({ width: width - 20, height: replInputHeight }); } focus(): void { @@ -382,12 +412,12 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati // --- Cached locals @memoize private get selectReplAction(): SelectReplAction { - return this.scopedInstantiationService.createInstance(SelectReplAction, SelectReplAction.ID, SelectReplAction.LABEL); + return this.instantiationService.createInstance(SelectReplAction, SelectReplAction.ID, SelectReplAction.LABEL); } @memoize private get clearReplAction(): ClearReplAction { - return this.scopedInstantiationService.createInstance(ClearReplAction, ClearReplAction.ID, ClearReplAction.LABEL); + return this.instantiationService.createInstance(ClearReplAction, ClearReplAction.ID, ClearReplAction.LABEL); } @memoize @@ -408,8 +438,7 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati // --- Creation - create(parent: HTMLElement): void { - super.create(parent); + protected renderBody(parent: HTMLElement): void { this.container = dom.append(parent, $('.repl')); const treeContainer = dom.append(this.container, $('.repl-tree')); this.createReplInput(this.container); @@ -483,7 +512,7 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati const lineCount = model ? Math.min(10, model.getLineCount()) : 1; if (lineCount !== this.replInputLineCount) { this.replInputLineCount = lineCount; - this.layout(this.dimension); + this.layoutBody(this.dimension.height, this.dimension.width); } })); // We add the input decoration only when the focus is in the input #61126 @@ -562,7 +591,7 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati this.replInput.setDecorations(DECORATION_KEY, decorations); } - protected saveState(): void { + saveState(): void { const replHistory = this.history.getHistory(); if (replHistory.length) { this.storageService.store(HISTORY_STORAGE_KEY, JSON.stringify(replHistory), StorageScope.WORKSPACE); @@ -684,8 +713,6 @@ class SelectReplAction extends Action { } else { await this.replService.selectSession(session); } - - return Promise.resolve(undefined); } } @@ -694,14 +721,14 @@ export class ClearReplAction extends Action { static readonly LABEL = localize('clearRepl', "Clear Console"); constructor(id: string, label: string, - @IPanelService private readonly panelService: IPanelService + @IViewsService private readonly viewsService: IViewsService ) { super(id, label, 'debug-action codicon-clear-all'); } async run(): Promise { - const repl = this.panelService.openPanel(REPL_ID); - await repl.clearRepl(); + const view = await this.viewsService.openView(REPL_VIEW_ID) as Repl; + await view.clearRepl(); aria.status(localize('debugConsoleCleared', "Debug console was cleared")); } } diff --git a/src/vs/workbench/contrib/debug/browser/startView.ts b/src/vs/workbench/contrib/debug/browser/startView.ts index f126f344d1..54d90394a8 100644 --- a/src/vs/workbench/contrib/debug/browser/startView.ts +++ b/src/vs/workbench/contrib/debug/browser/startView.ts @@ -14,7 +14,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { localize } from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { StartAction, RunAction, ConfigureAction } from 'vs/workbench/contrib/debug/browser/debugActions'; +import { StartAction, ConfigureAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { IDebugService } from 'vs/workbench/contrib/debug/common/debug'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; @@ -23,15 +23,38 @@ import { equals } from 'vs/base/common/arrays'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; const $ = dom.$; +interface DebugStartMetrics { + debuggers?: string[]; +} +type DebugStartMetricsClassification = { + debuggers?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; +}; + +function createClickElement(textContent: string, action: () => any): HTMLSpanElement { + const clickElement = $('span.click'); + clickElement.textContent = textContent; + clickElement.onclick = action; + clickElement.tabIndex = 0; + clickElement.onkeyup = (e) => { + const keyboardEvent = new StandardKeyboardEvent(e); + if (keyboardEvent.keyCode === KeyCode.Enter || (keyboardEvent.keyCode === KeyCode.Space)) { + action(); + } + }; + + return clickElement; +} + export class StartView extends ViewPane { static ID = 'workbench.debug.startView'; static LABEL = localize('start', "Start"); private debugButton!: Button; - private runButton!: Button; private firstMessageContainer!: HTMLElement; private secondMessageContainer!: HTMLElement; private clickElement: HTMLElement | undefined; @@ -48,9 +71,11 @@ export class StartView extends ViewPane { @IDebugService private readonly debugService: IDebugService, @IEditorService private readonly editorService: IEditorService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @IFileDialogService private readonly dialogService: IFileDialogService + @IFileDialogService private readonly dialogService: IFileDialogService, + @IInstantiationService instantiationService: IInstantiationService, + @ITelemetryService private readonly telemetryService: ITelemetryService ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: localize('debugStart', "Debug Start Section") }, keybindingService, contextMenuService, configurationService, contextKeyService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: localize('debugStart', "Debug Start Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, instantiationService); this._register(editorService.onDidActiveEditorChange(() => this.updateView())); this._register(this.debugService.getConfigurationManager().onDidRegisterDebugger(() => this.updateView())); } @@ -63,21 +88,13 @@ export class StartView extends ViewPane { const enabled = this.debuggerLabels.length > 0; this.debugButton.enabled = enabled; - this.runButton.enabled = enabled; const debugKeybinding = this.keybindingService.lookupKeybinding(StartAction.ID); - let debugLabel = this.debuggerLabels.length !== 1 ? localize('debug', "Debug") : localize('debugWith', "Debug with {0}", this.debuggerLabels[0]); + let debugLabel = this.debuggerLabels.length !== 1 ? localize('debug', "Run and Debug") : localize('debugWith', "Run and Debug {0}", this.debuggerLabels[0]); if (debugKeybinding) { debugLabel += ` (${debugKeybinding.getLabel()})`; } this.debugButton.label = debugLabel; - let runLabel = this.debuggerLabels.length !== 1 ? localize('run', "Run") : localize('runWith', "Run with {0}", this.debuggerLabels[0]); - const runKeybinding = this.keybindingService.lookupKeybinding(RunAction.ID); - if (runKeybinding) { - runLabel += ` (${runKeybinding.getLabel()})`; - } - this.runButton.label = runLabel; - const emptyWorkbench = this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY; this.firstMessageContainer.innerHTML = ''; this.secondMessageContainer.innerHTML = ''; @@ -85,13 +102,19 @@ export class StartView extends ViewPane { this.secondMessageContainer.appendChild(secondMessageElement); const setSecondMessage = () => { - secondMessageElement.textContent = localize('specifyHowToRun', "To further configure Debug and Run"); - this.clickElement = this.createClickElement(localize('configure', " create a launch.json file."), () => this.commandService.executeCommand(ConfigureAction.ID)); + secondMessageElement.textContent = localize('specifyHowToRun', "To customize Run and Debug"); + this.clickElement = createClickElement(localize('configure', " create a launch.json file."), () => { + this.telemetryService.publicLog2('debugStart.configure', { debuggers: this.debuggerLabels }); + this.commandService.executeCommand(ConfigureAction.ID); + }); this.secondMessageContainer.appendChild(this.clickElement); }; const setSecondMessageWithFolder = () => { - secondMessageElement.textContent = localize('noLaunchConfiguration', "To further configure Debug and Run, "); - this.clickElement = this.createClickElement(localize('openFolder', " open a folder"), () => this.dialogService.pickFolderAndOpen({ forceNewWindow: false })); + secondMessageElement.textContent = localize('noLaunchConfiguration', "To customize Run and Debug, "); + this.clickElement = createClickElement(localize('openFolder', " open a folder"), () => { + this.telemetryService.publicLog2('debugStart.openFolder', { debuggers: this.debuggerLabels }); + this.dialogService.pickFolderAndOpen({ forceNewWindow: false }); + }); this.secondMessageContainer.appendChild(this.clickElement); const moreText = $('span.moreText'); @@ -116,7 +139,10 @@ export class StartView extends ViewPane { } if (!enabled && emptyWorkbench) { - this.clickElement = this.createClickElement(localize('openFile', "Open a file"), () => this.dialogService.pickFileAndOpen({ forceNewWindow: false })); + this.clickElement = createClickElement(localize('openFile', "Open a file"), () => { + this.telemetryService.publicLog2('debugStart.openFile'); + this.dialogService.pickFileAndOpen({ forceNewWindow: false }); + }); this.firstMessageContainer.appendChild(this.clickElement); const firstMessageElement = $('span'); this.firstMessageContainer.appendChild(firstMessageElement); @@ -127,21 +153,6 @@ export class StartView extends ViewPane { } } - private createClickElement(textContent: string, action: () => any): HTMLSpanElement { - const clickElement = $('span.click'); - clickElement.textContent = textContent; - clickElement.onclick = action; - clickElement.tabIndex = 0; - clickElement.onkeyup = (e) => { - const keyboardEvent = new StandardKeyboardEvent(e); - if (keyboardEvent.keyCode === KeyCode.Enter || (keyboardEvent.keyCode === KeyCode.Space)) { - action(); - } - }; - - return clickElement; - } - protected renderBody(container: HTMLElement): void { this.firstMessageContainer = $('.top-section'); container.appendChild(this.firstMessageContainer); @@ -149,17 +160,11 @@ export class StartView extends ViewPane { this.debugButton = new Button(container); this._register(this.debugButton.onDidClick(() => { this.commandService.executeCommand(StartAction.ID); + this.telemetryService.publicLog2('debugStart.runAndDebug', { debuggers: this.debuggerLabels }); })); attachButtonStyler(this.debugButton, this.themeService); - this.runButton = new Button(container); - this.runButton.label = localize('run', "Run"); - dom.addClass(container, 'debug-start-view'); - this._register(this.runButton.onDidClick(() => { - this.commandService.executeCommand(RunAction.ID); - })); - attachButtonStyler(this.runButton, this.themeService); this.secondMessageContainer = $('.section'); container.appendChild(this.secondMessageContainer); diff --git a/src/vs/workbench/contrib/debug/browser/statusbarColorProvider.ts b/src/vs/workbench/contrib/debug/browser/statusbarColorProvider.ts index db9aa14ac1..2cb87cbcbf 100644 --- a/src/vs/workbench/contrib/debug/browser/statusbarColorProvider.ts +++ b/src/vs/workbench/contrib/debug/browser/statusbarColorProvider.ts @@ -67,7 +67,7 @@ export class StatusBarColorProvider extends Themable implements IWorkbenchContri // Container Colors const backgroundColor = this.getColor(this.getColorKey(STATUS_BAR_NO_FOLDER_BACKGROUND, STATUS_BAR_DEBUGGING_BACKGROUND, STATUS_BAR_BACKGROUND)); container.style.backgroundColor = backgroundColor || ''; - container.style.color = this.getColor(this.getColorKey(STATUS_BAR_NO_FOLDER_FOREGROUND, STATUS_BAR_DEBUGGING_FOREGROUND, STATUS_BAR_FOREGROUND)); + container.style.color = this.getColor(this.getColorKey(STATUS_BAR_NO_FOLDER_FOREGROUND, STATUS_BAR_DEBUGGING_FOREGROUND, STATUS_BAR_FOREGROUND)) || ''; // Border Color const borderColor = this.getColor(this.getColorKey(STATUS_BAR_NO_FOLDER_BORDER, STATUS_BAR_DEBUGGING_BORDER, STATUS_BAR_BORDER)) || this.getColor(contrastBorder); @@ -103,7 +103,7 @@ export class StatusBarColorProvider extends Themable implements IWorkbenchContri } } -export function isStatusbarInDebugMode(debugService: IDebugService): boolean { +function isStatusbarInDebugMode(debugService: IDebugService): boolean { if (debugService.state === State.Inactive || debugService.state === State.Initializing) { return false; } diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index 694cbdf6cc..97f0c0760a 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -50,11 +50,11 @@ export class VariablesView extends ViewPane { @IDebugService private readonly debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService, @IConfigurationService configurationService: IConfigurationService, - @IInstantiationService private readonly instantiationService: IInstantiationService, + @IInstantiationService instantiationService: IInstantiationService, @IClipboardService private readonly clipboardService: IClipboardService, @IContextKeyService contextKeyService: IContextKeyService ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('variablesSection', "Variables Section") }, keybindingService, contextMenuService, configurationService, contextKeyService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('variablesSection', "Variables Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, instantiationService); // 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 bd75a68698..0378a30429 100644 --- a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts +++ b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts @@ -33,6 +33,8 @@ import { dispose } from 'vs/base/common/lifecycle'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024; +let ignoreVariableSetEmitter = false; +let useCachedEvaluation = false; export class WatchExpressionsView extends ViewPane { @@ -45,11 +47,11 @@ export class WatchExpressionsView extends ViewPane { @IContextMenuService contextMenuService: IContextMenuService, @IDebugService private readonly debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService, - @IInstantiationService private readonly instantiationService: IInstantiationService, + @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService, ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('watchExpressionsSection', "Watch Expressions Section") }, keybindingService, contextMenuService, configurationService, contextKeyService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('watchExpressionsSection', "Watch Expressions Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, instantiationService); this.onWatchExpressionsUpdatedScheduler = new RunOnceScheduler(() => { this.needsRefresh = false; @@ -67,7 +69,16 @@ export class WatchExpressionsView extends ViewPane { ariaLabel: nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'watchAriaTreeLabel' }, "Debug Watch Expressions"), accessibilityProvider: new WatchExpressionsAccessibilityProvider(), identityProvider: { getId: (element: IExpression) => element.getId() }, - keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: IExpression) => e }, + keyboardNavigationLabelProvider: { + getKeyboardNavigationLabel: (e: IExpression) => { + if (e === this.debugService.getViewModel().getSelectedExpression()) { + // Don't filter input box + return undefined; + } + + return e; + } + }, dnd: new WatchExpressionsDragAndDrop(this.debugService), overrideStyles: { listBackground: SIDE_BAR_BACKGROUND @@ -90,7 +101,12 @@ export class WatchExpressionsView extends ViewPane { if (!this.isBodyVisible()) { this.needsRefresh = true; } else { + if (we && !we.name) { + // We are adding a new input box, no need to re-evaluate watch expressions + useCachedEvaluation = true; + } await this.tree.updateChildren(); + useCachedEvaluation = false; if (we instanceof Expression) { this.tree.reveal(we); } @@ -106,7 +122,11 @@ export class WatchExpressionsView extends ViewPane { this.onWatchExpressionsUpdatedScheduler.schedule(); } })); - this._register(variableSetEmitter.event(() => this.tree.updateChildren())); + this._register(variableSetEmitter.event(() => { + if (!ignoreVariableSetEmitter) { + this.tree.updateChildren(); + } + })); this._register(this.onDidChangeBodyVisibility(visible => { if (visible && this.needsRefresh) { @@ -192,7 +212,7 @@ export class WatchExpressionsView extends ViewPane { class WatchExpressionsDelegate implements IListVirtualDelegate { - getHeight(element: IExpression): number { + getHeight(_element: IExpression): number { return 22; } @@ -221,7 +241,7 @@ class WatchExpressionsDataSource implements IAsyncDataSource !!we.name + return Promise.all(watchExpressions.map(we => !!we.name && !useCachedEvaluation ? we.evaluate(viewModel.focusedSession!, viewModel.focusedStackFrame!, 'watch').then(() => we) : Promise.resolve(we))); } @@ -259,7 +279,9 @@ export class WatchExpressionsRenderer extends AbstractExpressionsRenderer { onFinish: (value: string, success: boolean) => { if (success && value) { this.debugService.renameWatchExpression(expression.getId(), value); + ignoreVariableSetEmitter = true; variableSetEmitter.fire(); + ignoreVariableSetEmitter = false; } else if (!expression.name) { this.debugService.removeWatchExpressions(expression.getId()); } diff --git a/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts b/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts index 82c40a4aae..1683afdb46 100644 --- a/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts +++ b/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts @@ -5,20 +5,19 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IDebugAdapter } from 'vs/workbench/contrib/debug/common/debug'; -import { timeout, Queue } from 'vs/base/common/async'; +import { timeout } from 'vs/base/common/async'; /** * Abstract implementation of the low level API for a debug adapter. * Missing is how this API communicates with the debug adapter. */ export abstract class AbstractDebugAdapter implements IDebugAdapter { - private sequence: number; private pendingRequests = new Map void>(); private requestCallback: ((request: DebugProtocol.Request) => void) | undefined; private eventCallback: ((request: DebugProtocol.Event) => void) | undefined; private messageCallback: ((message: DebugProtocol.ProtocolMessage) => void) | undefined; - private readonly queue = new Queue(); + private queue: DebugProtocol.ProtocolMessage[] = []; protected readonly _onError = new Emitter(); protected readonly _onExit = new Emitter(); @@ -64,8 +63,7 @@ export abstract class AbstractDebugAdapter implements IDebugAdapter { sendResponse(response: DebugProtocol.Response): void { if (response.seq > 0) { this._onError.fire(new Error(`attempt to send more than one response for command ${response.command}`)); - } - else { + } else { this.internalSend('response', response); } } @@ -107,35 +105,73 @@ export abstract class AbstractDebugAdapter implements IDebugAdapter { acceptMessage(message: DebugProtocol.ProtocolMessage): void { if (this.messageCallback) { this.messageCallback(message); + } else { + this.queue.push(message); + if (this.queue.length === 1) { + // first item = need to start processing loop + this.processQueue(); + } } - else { - this.queue.queue(() => { - switch (message.type) { - case 'event': - if (this.eventCallback) { - this.eventCallback(message); - } - break; - case 'request': - if (this.requestCallback) { - this.requestCallback(message); - } - break; - case 'response': - const response = message; - const clb = this.pendingRequests.get(response.request_seq); - if (clb) { - this.pendingRequests.delete(response.request_seq); - clb(response); - } - break; - } + } - // Artificially queueing protocol messages guarantees that any microtasks for - // previous message finish before next message is processed. This is essential - // to guarantee ordering when using promises anywhere along the call path. - return timeout(0); - }); + /** + * Returns whether we should insert a timeout between processing messageA + * and messageB. Artificially queueing protocol messages guarantees that any + * microtasks for previous message finish before next message is processed. + * This is essential ordering when using promises anywhere along the call path. + * + * For example, take the following, where `chooseAndSendGreeting` returns + * a person name and then emits a greeting event: + * + * ``` + * let person: string; + * adapter.onGreeting(() => console.log('hello', person)); + * person = await adapter.chooseAndSendGreeting(); + * ``` + * + * Because the event is dispatched synchronously, it may fire before person + * is assigned if they're processed in the same task. Inserting a task + * boundary avoids this issue. + */ + protected needsTaskBoundaryBetween(messageA: DebugProtocol.ProtocolMessage, messageB: DebugProtocol.ProtocolMessage) { + return messageA.type !== 'event' || messageB.type !== 'event'; + } + + /** + * Reads and dispatches items from the queue until it is empty. + */ + private async processQueue() { + let message: DebugProtocol.ProtocolMessage | undefined; + while (this.queue.length) { + if (!message || this.needsTaskBoundaryBetween(this.queue[0], message)) { + await timeout(0); + } + + message = this.queue.shift(); + if (!message) { + return; // may have been disposed of + } + + switch (message.type) { + case 'event': + if (this.eventCallback) { + this.eventCallback(message); + } + break; + case 'request': + if (this.requestCallback) { + this.requestCallback(message); + } + break; + case 'response': + const response = message; + const clb = this.pendingRequests.get(response.request_seq); + if (clb) { + this.pendingRequests.delete(response.request_seq); + clb(response); + } + break; + } } } @@ -172,6 +208,6 @@ export abstract class AbstractDebugAdapter implements IDebugAdapter { } dispose(): void { - this.queue.dispose(); + this.queue = []; } } diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index b064c72f55..36419dc1ff 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -13,7 +13,6 @@ import * as editorCommon from 'vs/editor/common/editorCommon'; import { ITextModel as EditorIModel } from 'vs/editor/common/model'; import { IEditor, ITextEditor } from 'vs/workbench/common/editor'; import { Position, IPosition } from 'vs/editor/common/core/position'; -import { CompletionItem } from 'vs/editor/common/modes'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import { Range, IRange } from 'vs/editor/common/core/range'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; @@ -32,7 +31,8 @@ export const WATCH_VIEW_ID = 'workbench.debug.watchExpressionsView'; export const CALLSTACK_VIEW_ID = 'workbench.debug.callStackView'; export const LOADED_SCRIPTS_VIEW_ID = 'workbench.debug.loadedScriptsView'; export const BREAKPOINTS_VIEW_ID = 'workbench.debug.breakPointsView'; -export const REPL_ID = 'workbench.panel.repl'; +export const DEBUG_PANEL_ID = 'workbench.panel.repl'; +export const REPL_VIEW_ID = 'workbench.panel.repl.view'; export const DEBUG_SERVICE_ID = 'debugService'; export const CONTEXT_DEBUG_TYPE = new RawContextKey('debugType', undefined); export const CONTEXT_DEBUG_CONFIGURATION_TYPE = new RawContextKey('debugConfigurationType', undefined); @@ -234,7 +234,7 @@ export interface IDebugSession extends ITreeElement { pause(threadId: number): Promise; terminateThreads(threadIds: number[]): Promise; - completions(frameId: number | undefined, text: string, position: Position, overwriteBefore: number, token: CancellationToken): Promise; + completions(frameId: number | undefined, text: string, position: Position, overwriteBefore: number, token: CancellationToken): Promise; setVariable(variablesReference: number | undefined, name: string, value: string): Promise; loadSource(resource: uri): Promise; getLoadedSources(): Promise; @@ -442,7 +442,7 @@ export interface IBreakpointsChangeEvent { added?: Array; removed?: Array; changed?: Array; - sessionOnly?: boolean; + sessionOnly: boolean; } // Debug configuration interfaces @@ -463,6 +463,7 @@ export interface IDebugConfiguration { fontFamily: string; lineHeight: number; wordWrap: boolean; + closeOnEnd: boolean; }; focusWindowOnBreak: boolean; onTaskErrors: 'debugAnyway' | 'showErrors' | 'prompt'; @@ -598,6 +599,7 @@ export interface IDebuggerContribution extends IPlatformSpecificAdapterContribut export interface IDebugConfigurationProvider { readonly type: string; resolveDebugConfiguration?(folderUri: uri | undefined, debugConfiguration: IConfig, token: CancellationToken): Promise; + resolveDebugConfigurationWithSubstitutedVariables?(folderUri: uri | undefined, debugConfiguration: IConfig, token: CancellationToken): Promise; provideDebugConfigurations?(folderUri: uri | undefined, token: CancellationToken): Promise; debugAdapterExecutable?(folderUri: uri | undefined): Promise; // TODO@AW legacy } diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 0b17b316ed..14e7a025ca 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -160,10 +160,6 @@ export class ExpressionContainer implements IExpressionContainer { this.session = session; try { const response = await session.evaluate(expression, stackFrame ? stackFrame.frameId : undefined, context); - if (response && response.success === false) { - this.value = response.message || ''; - return false; - } if (response && response.body) { this.value = response.body.result || ''; @@ -1014,7 +1010,7 @@ export class DebugModel implements IDebugModel { this.sortAndDeDup(); if (fireEvent) { - this._onDidChangeBreakpoints.fire({ added: newBreakpoints }); + this._onDidChangeBreakpoints.fire({ added: newBreakpoints, sessionOnly: false }); } return newBreakpoints; @@ -1022,7 +1018,7 @@ export class DebugModel implements IDebugModel { removeBreakpoints(toRemove: IBreakpoint[]): void { this.breakpoints = this.breakpoints.filter(bp => !toRemove.some(toRemove => toRemove.getId() === bp.getId())); - this._onDidChangeBreakpoints.fire({ removed: toRemove }); + this._onDidChangeBreakpoints.fire({ removed: toRemove, sessionOnly: false }); } updateBreakpoints(data: Map): void { @@ -1035,7 +1031,7 @@ export class DebugModel implements IDebugModel { } }); this.sortAndDeDup(); - this._onDidChangeBreakpoints.fire({ changed: updated }); + this._onDidChangeBreakpoints.fire({ changed: updated, sessionOnly: false }); } setBreakpointSessionData(sessionId: string, capabilites: DebugProtocol.Capabilities, data: Map | undefined): void { @@ -1104,7 +1100,7 @@ export class DebugModel implements IDebugModel { this.breakpointsActivated = true; } - this._onDidChangeBreakpoints.fire({ changed: changed }); + this._onDidChangeBreakpoints.fire({ changed: changed, sessionOnly: false }); } } @@ -1133,13 +1129,13 @@ export class DebugModel implements IDebugModel { this.breakpointsActivated = true; } - this._onDidChangeBreakpoints.fire({ changed: changed }); + this._onDidChangeBreakpoints.fire({ changed: changed, sessionOnly: false }); } addFunctionBreakpoint(functionName: string, id?: string): IFunctionBreakpoint { const newFunctionBreakpoint = new FunctionBreakpoint(functionName, true, undefined, undefined, undefined, id); this.functionBreakpoints.push(newFunctionBreakpoint); - this._onDidChangeBreakpoints.fire({ added: [newFunctionBreakpoint] }); + this._onDidChangeBreakpoints.fire({ added: [newFunctionBreakpoint], sessionOnly: false }); return newFunctionBreakpoint; } @@ -1148,7 +1144,7 @@ export class DebugModel implements IDebugModel { const functionBreakpoint = this.functionBreakpoints.filter(fbp => fbp.getId() === id).pop(); if (functionBreakpoint) { functionBreakpoint.name = name; - this._onDidChangeBreakpoints.fire({ changed: [functionBreakpoint] }); + this._onDidChangeBreakpoints.fire({ changed: [functionBreakpoint], sessionOnly: false }); } } @@ -1161,13 +1157,13 @@ export class DebugModel implements IDebugModel { removed = this.functionBreakpoints; this.functionBreakpoints = []; } - this._onDidChangeBreakpoints.fire({ removed }); + this._onDidChangeBreakpoints.fire({ removed, sessionOnly: false }); } addDataBreakpoint(label: string, dataId: string, canPersist: boolean, accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined): void { const newDataBreakpoint = new DataBreakpoint(label, dataId, canPersist, true, undefined, undefined, undefined, accessTypes); this.dataBreakopints.push(newDataBreakpoint); - this._onDidChangeBreakpoints.fire({ added: [newDataBreakpoint] }); + this._onDidChangeBreakpoints.fire({ added: [newDataBreakpoint], sessionOnly: false }); } removeDataBreakpoints(id?: string): void { @@ -1179,15 +1175,15 @@ export class DebugModel implements IDebugModel { removed = this.dataBreakopints; this.dataBreakopints = []; } - this._onDidChangeBreakpoints.fire({ removed }); + this._onDidChangeBreakpoints.fire({ removed, sessionOnly: false }); } getWatchExpressions(): Expression[] { return this.watchExpressions; } - addWatchExpression(name: string): IExpression { - const we = new Expression(name); + addWatchExpression(name?: string): IExpression { + const we = new Expression(name || ''); this.watchExpressions.push(we); this._onDidChangeWatchExpressions.fire(we); diff --git a/src/vs/workbench/contrib/debug/common/debugSchemas.ts b/src/vs/workbench/contrib/debug/common/debugSchemas.ts index c60e484ef4..28325a50b9 100644 --- a/src/vs/workbench/contrib/debug/common/debugSchemas.ts +++ b/src/vs/workbench/contrib/debug/common/debugSchemas.ts @@ -21,7 +21,7 @@ export const debuggersExtPoint = extensionsRegistry.ExtensionsRegistry.registerE items: { additionalProperties: false, type: 'object', - defaultSnippets: [{ body: { type: '', program: '', runtime: '', enableBreakpointsFor: { languageIds: [''] } } }], + defaultSnippets: [{ body: { type: '', program: '', runtime: '' } }], properties: { type: { description: nls.localize('vscode.extension.contributes.debuggers.type', "Unique identifier for this debug adapter."), @@ -59,10 +59,6 @@ export const debuggersExtPoint = extensionsRegistry.ExtensionsRegistry.registerE description: nls.localize('vscode.extension.contributes.debuggers.languages', "List of languages for which the debug extension could be considered the \"default debugger\"."), type: 'array' }, - adapterExecutableCommand: { - description: nls.localize('vscode.extension.contributes.debuggers.adapterExecutableCommand', "If specified VS Code will call this command to determine the executable path of the debug adapter and the arguments to pass."), - type: 'string' - }, configurationSnippets: { description: nls.localize('vscode.extension.contributes.debuggers.configurationSnippets', "Snippets for adding new configurations in \'launch.json\'."), type: 'array' diff --git a/src/vs/workbench/contrib/debug/common/debugUtils.ts b/src/vs/workbench/contrib/debug/common/debugUtils.ts index 29c87bf571..4095c4408f 100644 --- a/src/vs/workbench/contrib/debug/common/debugUtils.ts +++ b/src/vs/workbench/contrib/debug/common/debugUtils.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { equalsIgnoreCase } from 'vs/base/common/strings'; -import { IDebuggerContribution, IDebugSession } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebuggerContribution, IDebugSession, IConfigPresentation } from 'vs/workbench/contrib/debug/common/debug'; import { URI as uri } from 'vs/base/common/uri'; import { isAbsolute } from 'vs/base/common/path'; import { deepClone } from 'vs/base/common/objects'; @@ -236,3 +236,31 @@ function convertPaths(msg: DebugProtocol.ProtocolMessage, fixSourcePath: (toDA: break; } } + +export function getVisibleAndSorted(array: T[]): T[] { + return array.filter(config => !config.presentation?.hidden).sort((first, second) => { + if (!first.presentation) { + return 1; + } + if (!second.presentation) { + return -1; + } + if (!first.presentation.group) { + return 1; + } + if (!second.presentation.group) { + return -1; + } + if (first.presentation.group !== second.presentation.group) { + return first.presentation.group.localeCompare(second.presentation.group); + } + if (typeof first.presentation.order !== 'number') { + return 1; + } + if (typeof second.presentation.order !== 'number') { + return -1; + } + + return first.presentation.order - second.presentation.order; + }); +} diff --git a/src/vs/workbench/contrib/debug/common/debugger.ts b/src/vs/workbench/contrib/debug/common/debugger.ts index 482ec4678f..72f57101bb 100644 --- a/src/vs/workbench/contrib/debug/common/debugger.ts +++ b/src/vs/workbench/contrib/debug/common/debugger.ts @@ -30,7 +30,10 @@ export class Debugger implements IDebugger { private mergedExtensionDescriptions: IExtensionDescription[] = []; private mainExtensionDescription: IExtensionDescription | undefined; - constructor(private configurationManager: IConfigurationManager, dbgContribution: IDebuggerContribution, extensionDescription: IExtensionDescription, + constructor( + private configurationManager: IConfigurationManager, + dbgContribution: IDebuggerContribution, + extensionDescription: IExtensionDescription, @IConfigurationService private readonly configurationService: IConfigurationService, @ITextResourcePropertiesService private readonly resourcePropertiesService: ITextResourcePropertiesService, @IConfigurationResolverService private readonly configurationResolverService: IConfigurationResolverService, @@ -41,7 +44,7 @@ export class Debugger implements IDebugger { this.merge(dbgContribution, extensionDescription); } - public merge(otherDebuggerContribution: IDebuggerContribution, extensionDescription: IExtensionDescription): void { + merge(otherDebuggerContribution: IDebuggerContribution, extensionDescription: IExtensionDescription): void { /** * Copies all properties of source into destination. The optional parameter "overwrite" allows to control @@ -92,7 +95,7 @@ export class Debugger implements IDebugger { } } - public createDebugAdapter(session: IDebugSession): Promise { + createDebugAdapter(session: IDebugSession): Promise { return this.configurationManager.activateDebuggers('onDebugAdapterProtocolTracker', this.type).then(_ => { const da = this.configurationManager.createDebugAdapter(session); if (da) { @@ -172,7 +175,7 @@ export class Debugger implements IDebugger { return Promise.resolve(content); } - public getMainExtensionDescriptor(): IExtensionDescription { + getMainExtensionDescriptor(): IExtensionDescription { return this.mainExtensionDescription || this.mergedExtensionDescriptions[0]; } diff --git a/src/vs/workbench/contrib/debug/node/terminals.ts b/src/vs/workbench/contrib/debug/node/terminals.ts index 224c45f664..a23e57c6c0 100644 --- a/src/vs/workbench/contrib/debug/node/terminals.ts +++ b/src/vs/workbench/contrib/debug/node/terminals.ts @@ -5,7 +5,6 @@ import * as cp from 'child_process'; import * as env from 'vs/base/common/platform'; -import { getSystemShell } from 'vs/workbench/contrib/terminal/node/terminal'; import { WindowsExternalTerminalService, MacExternalTerminalService, LinuxExternalTerminalService } from 'vs/workbench/contrib/externalTerminal/node/externalTerminalService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IExternalTerminalService } from 'vs/workbench/contrib/externalTerminal/common/externalTerminal'; @@ -76,35 +75,22 @@ export function hasChildProcesses(processId: number | undefined): Promise= 0 || shell.indexOf('pwsh') >= 0) { shellType = ShellType.powershell; } else if (shell.indexOf('cmd.exe') >= 0) { shellType = ShellType.cmd; } else if (shell.indexOf('bash') >= 0) { shellType = ShellType.bash; + } else if (env.isWindows) { + shellType = ShellType.cmd; // pick a good default for Windows + } else { + shellType = ShellType.bash; // pick a good default for anything else } let quote: (s: string) => string; diff --git a/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts b/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts index ed174a03ed..a9519b89a6 100644 --- a/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { replaceWhitespace, renderExpressionValue, renderVariable } from 'vs/workbench/contrib/debug/browser/baseDebugView'; +import { replaceWhitespace, renderExpressionValue, renderVariable, renderViewTree } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import * as dom from 'vs/base/browser/dom'; import { Expression, Variable, Scope, StackFrame, Thread } from 'vs/workbench/contrib/debug/common/debugModel'; import { MockSession } from 'vs/workbench/contrib/debug/test/common/mockDebug'; @@ -32,6 +32,16 @@ suite('Debug - Base Debug View', () => { assert.equal(replaceWhitespace('hey \r\t\n\t\t\n there'), 'hey \\r\\t\\n\\t\\t\\n there'); }); + test('render view tree', () => { + const container = $('.container'); + const treeContainer = renderViewTree(container); + + assert.equal(treeContainer.className, 'debug-view-content'); + assert.equal(container.childElementCount, 1); + assert.equal(container.firstChild, treeContainer); + assert.equal(treeContainer instanceof HTMLDivElement, true); + }); + test.skip('render expression value', () => { // {{SQL CARBON EDIT}} skip test let container = $('.container'); renderExpressionValue('render \n me', container, { showHover: true, preserveWhitespace: true }); diff --git a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts new file mode 100644 index 0000000000..8e8bf57e07 --- /dev/null +++ b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts @@ -0,0 +1,349 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { URI as uri } from 'vs/base/common/uri'; +import { DebugModel, Breakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; +import { DebugSession } from 'vs/workbench/contrib/debug/browser/debugSession'; +import { NullOpenerService } from 'vs/platform/opener/common/opener'; +import { getExpandedBodySize, getBreakpointMessageAndClassName } from 'vs/workbench/contrib/debug/browser/breakpointsView'; +import { dispose } from 'vs/base/common/lifecycle'; +import { Range } from 'vs/editor/common/core/range'; +import { IBreakpointData, IDebugSessionOptions, IBreakpointUpdateData, State } from 'vs/workbench/contrib/debug/common/debug'; +import { TextModel } from 'vs/editor/common/model/textModel'; +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'; + +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!); +} + +function addBreakpointsAndCheckEvents(model: DebugModel, uri: uri, data: IBreakpointData[]): void { + let eventCount = 0; + const toDispose = model.onDidChangeBreakpoints(e => { + assert.equal(e?.sessionOnly, false); + assert.equal(e?.changed, undefined); + assert.equal(e?.removed, undefined); + const added = e?.added; + assert.notEqual(added, undefined); + assert.equal(added!.length, data.length); + eventCount++; + dispose(toDispose); + for (let i = 0; i < data.length; i++) { + assert.equal(e!.added![i] instanceof Breakpoint, true); + assert.equal((e!.added![i] as Breakpoint).lineNumber, data[i].lineNumber); + } + }); + model.addBreakpoints(uri, data); + assert.equal(eventCount, 1); +} + +suite('Debug - Breakpoints', () => { + let model: DebugModel; + + setup(() => { + model = new DebugModel([], [], [], [], [], { isDirty: (e: any) => false }); + }); + + // Breakpoints + + test('simple', () => { + const modelUri = uri.file('/myfolder/myfile.js'); + + addBreakpointsAndCheckEvents(model, modelUri, [{ lineNumber: 5, enabled: true }, { lineNumber: 10, enabled: false }]); + assert.equal(model.areBreakpointsActivated(), true); + assert.equal(model.getBreakpoints().length, 2); + + let eventCount = 0; + const toDispose = model.onDidChangeBreakpoints(e => { + eventCount++; + assert.equal(e?.added, undefined); + assert.equal(e?.sessionOnly, false); + assert.equal(e?.removed?.length, 2); + assert.equal(e?.changed, undefined); + + dispose(toDispose); + }); + + model.removeBreakpoints(model.getBreakpoints()); + assert.equal(eventCount, 1); + assert.equal(model.getBreakpoints().length, 0); + }); + + test('toggling', () => { + const modelUri = uri.file('/myfolder/myfile.js'); + + addBreakpointsAndCheckEvents(model, modelUri, [{ lineNumber: 5, enabled: true }, { lineNumber: 10, enabled: false }]); + addBreakpointsAndCheckEvents(model, modelUri, [{ lineNumber: 12, enabled: true, condition: 'fake condition' }]); + assert.equal(model.getBreakpoints().length, 3); + const bp = model.getBreakpoints().pop(); + if (bp) { + model.removeBreakpoints([bp]); + } + assert.equal(model.getBreakpoints().length, 2); + + model.setBreakpointsActivated(false); + assert.equal(model.areBreakpointsActivated(), false); + model.setBreakpointsActivated(true); + assert.equal(model.areBreakpointsActivated(), true); + }); + + test('two files', () => { + const modelUri1 = uri.file('/myfolder/my file first.js'); + const modelUri2 = uri.file('/secondfolder/second/second file.js'); + addBreakpointsAndCheckEvents(model, modelUri1, [{ lineNumber: 5, enabled: true }, { lineNumber: 10, enabled: false }]); + assert.equal(getExpandedBodySize(model), 44); + + addBreakpointsAndCheckEvents(model, modelUri2, [{ lineNumber: 1, enabled: true }, { lineNumber: 2, enabled: true }, { lineNumber: 3, enabled: false }]); + assert.equal(getExpandedBodySize(model), 110); + + assert.equal(model.getBreakpoints().length, 5); + assert.equal(model.getBreakpoints({ uri: modelUri1 }).length, 2); + assert.equal(model.getBreakpoints({ uri: modelUri2 }).length, 3); + assert.equal(model.getBreakpoints({ lineNumber: 5 }).length, 1); + assert.equal(model.getBreakpoints({ column: 5 }).length, 0); + + const bp = model.getBreakpoints()[0]; + const update = new Map(); + update.set(bp.getId(), { lineNumber: 100 }); + let eventFired = false; + const toDispose = model.onDidChangeBreakpoints(e => { + eventFired = true; + assert.equal(e?.added, undefined); + assert.equal(e?.removed, undefined); + assert.equal(e?.changed?.length, 1); + dispose(toDispose); + }); + model.updateBreakpoints(update); + assert.equal(eventFired, true); + assert.equal(bp.lineNumber, 100); + + assert.equal(model.getBreakpoints({ enabledOnly: true }).length, 3); + model.enableOrDisableAllBreakpoints(false); + model.getBreakpoints().forEach(bp => { + assert.equal(bp.enabled, false); + }); + assert.equal(model.getBreakpoints({ enabledOnly: true }).length, 0); + + model.setEnablement(bp, true); + assert.equal(bp.enabled, true); + + model.removeBreakpoints(model.getBreakpoints({ uri: modelUri1 })); + assert.equal(getExpandedBodySize(model), 66); + + assert.equal(model.getBreakpoints().length, 3); + }); + + test('conditions', () => { + const modelUri1 = uri.file('/myfolder/my file first.js'); + addBreakpointsAndCheckEvents(model, modelUri1, [{ lineNumber: 5, condition: 'i < 5', hitCondition: '17' }, { lineNumber: 10, condition: 'j < 3' }]); + const breakpoints = model.getBreakpoints(); + + assert.equal(breakpoints[0].condition, 'i < 5'); + assert.equal(breakpoints[0].hitCondition, '17'); + assert.equal(breakpoints[1].condition, 'j < 3'); + assert.equal(!!breakpoints[1].hitCondition, false); + + assert.equal(model.getBreakpoints().length, 2); + model.removeBreakpoints(model.getBreakpoints()); + assert.equal(model.getBreakpoints().length, 0); + }); + + test('function breakpoints', () => { + model.addFunctionBreakpoint('foo', '1'); + model.addFunctionBreakpoint('bar', '2'); + model.renameFunctionBreakpoint('1', 'fooUpdated'); + model.renameFunctionBreakpoint('2', 'barUpdated'); + + const functionBps = model.getFunctionBreakpoints(); + assert.equal(functionBps[0].name, 'fooUpdated'); + assert.equal(functionBps[1].name, 'barUpdated'); + + model.removeFunctionBreakpoints(); + assert.equal(model.getFunctionBreakpoints().length, 0); + }); + + test('multiple sessions', () => { + const modelUri = uri.file('/myfolder/myfile.js'); + addBreakpointsAndCheckEvents(model, modelUri, [{ lineNumber: 5, enabled: true, condition: 'x > 5' }, { lineNumber: 10, enabled: false }]); + const breakpoints = model.getBreakpoints(); + const session = createMockSession(model); + const data = new Map(); + + assert.equal(breakpoints[0].lineNumber, 5); + assert.equal(breakpoints[1].lineNumber, 10); + + data.set(breakpoints[0].getId(), { verified: false, line: 10 }); + data.set(breakpoints[1].getId(), { verified: true, line: 50 }); + model.setBreakpointSessionData(session.getId(), {}, data); + assert.equal(breakpoints[0].lineNumber, 5); + assert.equal(breakpoints[1].lineNumber, 50); + + const session2 = createMockSession(model); + const data2 = new Map(); + data2.set(breakpoints[0].getId(), { verified: true, line: 100 }); + data2.set(breakpoints[1].getId(), { verified: true, line: 500 }); + model.setBreakpointSessionData(session2.getId(), {}, data2); + + // Breakpoint is verified only once, show that line + assert.equal(breakpoints[0].lineNumber, 100); + // Breakpoint is verified two times, show the original line + assert.equal(breakpoints[1].lineNumber, 10); + + model.setBreakpointSessionData(session.getId(), {}, undefined); + // No more double session verification + assert.equal(breakpoints[0].lineNumber, 100); + assert.equal(breakpoints[1].lineNumber, 500); + + assert.equal(breakpoints[0].supported, false); + const data3 = new Map(); + data3.set(breakpoints[0].getId(), { verified: true, line: 500 }); + model.setBreakpointSessionData(session2.getId(), { supportsConditionalBreakpoints: true }, data2); + assert.equal(breakpoints[0].supported, true); + }); + + test('exception breakpoints', () => { + let eventCount = 0; + model.onDidChangeBreakpoints(() => eventCount++); + model.setExceptionBreakpoints([{ filter: 'uncaught', label: 'UNCAUGHT', default: true }]); + assert.equal(eventCount, 1); + let exceptionBreakpoints = model.getExceptionBreakpoints(); + assert.equal(exceptionBreakpoints.length, 1); + assert.equal(exceptionBreakpoints[0].filter, 'uncaught'); + assert.equal(exceptionBreakpoints[0].enabled, true); + + model.setExceptionBreakpoints([{ filter: 'uncaught', label: 'UNCAUGHT' }, { filter: 'caught', label: 'CAUGHT' }]); + assert.equal(eventCount, 2); + exceptionBreakpoints = model.getExceptionBreakpoints(); + assert.equal(exceptionBreakpoints.length, 2); + assert.equal(exceptionBreakpoints[0].filter, 'uncaught'); + assert.equal(exceptionBreakpoints[0].enabled, true); + assert.equal(exceptionBreakpoints[1].filter, 'caught'); + assert.equal(exceptionBreakpoints[1].label, 'CAUGHT'); + assert.equal(exceptionBreakpoints[1].enabled, false); + }); + + test('data breakpoints', () => { + let eventCount = 0; + model.onDidChangeBreakpoints(() => eventCount++); + + model.addDataBreakpoint('label', 'id', true, ['read']); + model.addDataBreakpoint('second', 'secondId', false, ['readWrite']); + const dataBreakpoints = model.getDataBreakpoints(); + assert.equal(dataBreakpoints[0].canPersist, true); + assert.equal(dataBreakpoints[0].dataId, 'id'); + assert.equal(dataBreakpoints[1].canPersist, false); + assert.equal(dataBreakpoints[1].description, 'second'); + + assert.equal(eventCount, 2); + + model.removeDataBreakpoints(dataBreakpoints[0].getId()); + assert.equal(eventCount, 3); + assert.equal(model.getDataBreakpoints().length, 1); + + model.removeDataBreakpoints(); + assert.equal(model.getDataBreakpoints().length, 0); + assert.equal(eventCount, 4); + }); + + test('message and class name', () => { + const modelUri = uri.file('/myfolder/my file first.js'); + addBreakpointsAndCheckEvents(model, modelUri, [ + { lineNumber: 5, enabled: true, condition: 'x > 5' }, + { lineNumber: 10, enabled: false }, + { lineNumber: 12, enabled: true, logMessage: 'hello' }, + { lineNumber: 15, enabled: true, hitCondition: '12' }, + { lineNumber: 500, enabled: true }, + ]); + const breakpoints = model.getBreakpoints(); + + let result = getBreakpointMessageAndClassName(State.Stopped, true, breakpoints[0]); + assert.equal(result.message, 'Expression: x > 5'); + assert.equal(result.className, 'codicon-debug-breakpoint-conditional'); + + result = getBreakpointMessageAndClassName(State.Stopped, true, breakpoints[1]); + assert.equal(result.message, 'Disabled Breakpoint'); + assert.equal(result.className, 'codicon-debug-breakpoint-disabled'); + + result = getBreakpointMessageAndClassName(State.Stopped, true, breakpoints[2]); + assert.equal(result.message, 'Log Message: hello'); + assert.equal(result.className, 'codicon-debug-breakpoint-log'); + + result = getBreakpointMessageAndClassName(State.Stopped, true, breakpoints[3]); + assert.equal(result.message, 'Hit Count: 12'); + assert.equal(result.className, 'codicon-debug-breakpoint-conditional'); + + result = getBreakpointMessageAndClassName(State.Stopped, true, breakpoints[4]); + assert.equal(result.message, 'Breakpoint'); + assert.equal(result.className, 'codicon-debug-breakpoint'); + + result = getBreakpointMessageAndClassName(State.Stopped, false, breakpoints[2]); + assert.equal(result.message, 'Disabled Logpoint'); + assert.equal(result.className, 'codicon-debug-breakpoint-log-disabled'); + + model.addDataBreakpoint('label', 'id', true, ['read']); + const dataBreakpoints = model.getDataBreakpoints(); + result = getBreakpointMessageAndClassName(State.Stopped, true, dataBreakpoints[0]); + assert.equal(result.message, 'Data Breakpoint'); + assert.equal(result.className, 'codicon-debug-breakpoint-data'); + + const functionBreakpoint = model.addFunctionBreakpoint('foo', '1'); + result = getBreakpointMessageAndClassName(State.Stopped, true, functionBreakpoint); + assert.equal(result.message, 'Function Breakpoint'); + assert.equal(result.className, 'codicon-debug-breakpoint-function'); + + const data = new Map(); + data.set(breakpoints[0].getId(), { verified: false, line: 10 }); + data.set(breakpoints[1].getId(), { verified: true, line: 50 }); + data.set(breakpoints[2].getId(), { verified: true, line: 50, message: 'world' }); + data.set(functionBreakpoint.getId(), { verified: true }); + model.setBreakpointSessionData('mocksessionid', { supportsFunctionBreakpoints: false, supportsDataBreakpoints: true, supportsLogPoints: true }, data); + + result = getBreakpointMessageAndClassName(State.Stopped, true, breakpoints[0]); + assert.equal(result.message, 'Unverified Breakpoint'); + assert.equal(result.className, 'codicon-debug-breakpoint-unverified'); + + result = getBreakpointMessageAndClassName(State.Stopped, true, functionBreakpoint); + assert.equal(result.message, 'Function breakpoints not supported by this debug type'); + assert.equal(result.className, 'codicon-debug-breakpoint-function-unverified'); + + result = getBreakpointMessageAndClassName(State.Stopped, true, breakpoints[2]); + assert.equal(result.message, 'Log Message: hello, world'); + assert.equal(result.className, 'codicon-debug-breakpoint-log'); + }); + + test('decorations', () => { + const modelUri = uri.file('/myfolder/my file first.js'); + const languageIdentifier = new LanguageIdentifier('testMode', LanguageId.PlainText); + const textModel = new TextModel( + ['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 + ); + addBreakpointsAndCheckEvents(model, modelUri, [ + { lineNumber: 1, enabled: true, condition: 'x > 5' }, + { lineNumber: 2, column: 4, enabled: false }, + { lineNumber: 3, enabled: true, logMessage: 'hello' }, + { lineNumber: 500, enabled: true }, + ]); + const breakpoints = model.getBreakpoints(); + + let decorations = createBreakpointDecorations(textModel, breakpoints, State.Running, true, true); + assert.equal(decorations.length, 3); // last breakpoint filtered out since it has a large line number + assert.deepEqual(decorations[0].range, new Range(1, 1, 1, 2)); + assert.deepEqual(decorations[1].range, new Range(2, 4, 2, 5)); + assert.deepEqual(decorations[2].range, new Range(3, 5, 3, 6)); + assert.equal(decorations[0].options.beforeContentClassName, undefined); + assert.equal(decorations[1].options.beforeContentClassName, `debug-breakpoint-placeholder`); + assert.equal(decorations[0].options.overviewRuler?.position, OverviewRulerLane.Left); + const expected = new MarkdownString().appendCodeblock(languageIdentifier.language, 'Expression: x > 5'); + assert.deepEqual(decorations[0].options.glyphMarginHoverMessage, expected); + + decorations = createBreakpointDecorations(textModel, breakpoints, State.Running, true, false); + assert.equal(decorations[0].options.overviewRuler, null); + }); +}); diff --git a/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts b/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts new file mode 100644 index 0000000000..2a4c97aa14 --- /dev/null +++ b/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts @@ -0,0 +1,419 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { DebugModel, StackFrame, Thread } from 'vs/workbench/contrib/debug/common/debugModel'; +import * as sinon from 'sinon'; +import { MockRawSession } from 'vs/workbench/contrib/debug/test/common/mockDebug'; +import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; +import { DebugSession } from 'vs/workbench/contrib/debug/browser/debugSession'; +import { Range } from 'vs/editor/common/core/range'; +import { IDebugSessionOptions, State } from 'vs/workbench/contrib/debug/common/debug'; +import { NullOpenerService } from 'vs/platform/opener/common/opener'; +import { createDecorationsForStackFrame } from 'vs/workbench/contrib/debug/browser/callStackEditorContribution'; +import { Constants } from 'vs/base/common/uint'; +import { getContext, getContextForContributedActions } from 'vs/workbench/contrib/debug/browser/callStackView'; +import { getStackFrameThreadAndSessionToFocus } from 'vs/workbench/contrib/debug/browser/debugService'; + +export 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!); +} + +function createTwoStackFrames(session: DebugSession): { firstStackFrame: StackFrame, secondStackFrame: StackFrame } { + let firstStackFrame: StackFrame; + let secondStackFrame: StackFrame; + const thread = new class extends Thread { + public getCallStack(): StackFrame[] { + return [firstStackFrame, secondStackFrame]; + } + }(session, 'mockthread', 1); + + const firstSource = new Source({ + name: 'internalModule.js', + path: 'a/b/c/d/internalModule.js', + sourceReference: 10, + }, 'aDebugSessionId'); + const secondSource = new Source({ + name: 'internalModule.js', + path: 'z/x/c/d/internalModule.js', + sourceReference: 11, + }, 'aDebugSessionId'); + + firstStackFrame = new StackFrame(thread, 1, firstSource, 'app.js', 'normal', { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 10 }, 1); + secondStackFrame = new StackFrame(thread, 1, secondSource, 'app.js', 'normal', { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 10 }, 1); + + return { firstStackFrame, secondStackFrame }; +} + +suite('Debug - CallStack', () => { + let model: DebugModel; + let rawSession: MockRawSession; + + setup(() => { + model = new DebugModel([], [], [], [], [], { isDirty: (e: any) => false }); + rawSession = new MockRawSession(); + }); + + // Threads + + test('threads simple', () => { + const threadId = 1; + const threadName = 'firstThread'; + const session = createMockSession(model); + model.addSession(session); + + assert.equal(model.getSessions(true).length, 1); + model.rawUpdate({ + sessionId: session.getId(), + threads: [{ + id: threadId, + name: threadName + }] + }); + + assert.equal(session.getThread(threadId)!.name, threadName); + + model.clearThreads(session.getId(), true); + assert.equal(session.getThread(threadId), undefined); + assert.equal(model.getSessions(true).length, 1); + }); + + test('threads multiple wtih allThreadsStopped', () => { + const threadId1 = 1; + const threadName1 = 'firstThread'; + const threadId2 = 2; + const threadName2 = 'secondThread'; + const stoppedReason = 'breakpoint'; + + // Add the threads + const session = createMockSession(model); + model.addSession(session); + + session['raw'] = rawSession; + + model.rawUpdate({ + sessionId: session.getId(), + threads: [{ + id: threadId1, + name: threadName1 + }] + }); + + // Stopped event with all threads stopped + model.rawUpdate({ + sessionId: session.getId(), + threads: [{ + id: threadId1, + name: threadName1 + }, { + id: threadId2, + name: threadName2 + }], + stoppedDetails: { + reason: stoppedReason, + threadId: 1, + allThreadsStopped: true + }, + }); + + const thread1 = session.getThread(threadId1)!; + const thread2 = session.getThread(threadId2)!; + + // at the beginning, callstacks are obtainable but not available + assert.equal(session.getAllThreads().length, 2); + assert.equal(thread1.name, threadName1); + assert.equal(thread1.stopped, true); + assert.equal(thread1.getCallStack().length, 0); + assert.equal(thread1.stoppedDetails!.reason, stoppedReason); + assert.equal(thread2.name, threadName2); + assert.equal(thread2.stopped, true); + assert.equal(thread2.getCallStack().length, 0); + assert.equal(thread2.stoppedDetails!.reason, undefined); + + // after calling getCallStack, the callstack becomes available + // and results in a request for the callstack in the debug adapter + thread1.fetchCallStack().then(() => { + assert.notEqual(thread1.getCallStack().length, 0); + }); + + thread2.fetchCallStack().then(() => { + assert.notEqual(thread2.getCallStack().length, 0); + }); + + // calling multiple times getCallStack doesn't result in multiple calls + // to the debug adapter + thread1.fetchCallStack().then(() => { + return thread2.fetchCallStack(); + }); + + // clearing the callstack results in the callstack not being available + thread1.clearCallStack(); + assert.equal(thread1.stopped, true); + assert.equal(thread1.getCallStack().length, 0); + + thread2.clearCallStack(); + assert.equal(thread2.stopped, true); + assert.equal(thread2.getCallStack().length, 0); + + model.clearThreads(session.getId(), true); + assert.equal(session.getThread(threadId1), undefined); + assert.equal(session.getThread(threadId2), undefined); + assert.equal(session.getAllThreads().length, 0); + }); + + test('threads mutltiple without allThreadsStopped', () => { + const sessionStub = sinon.spy(rawSession, 'stackTrace'); + + const stoppedThreadId = 1; + const stoppedThreadName = 'stoppedThread'; + const runningThreadId = 2; + const runningThreadName = 'runningThread'; + const stoppedReason = 'breakpoint'; + const session = createMockSession(model); + model.addSession(session); + + session['raw'] = rawSession; + + // Add the threads + model.rawUpdate({ + sessionId: session.getId(), + threads: [{ + id: stoppedThreadId, + name: stoppedThreadName + }] + }); + + // Stopped event with only one thread stopped + model.rawUpdate({ + sessionId: session.getId(), + threads: [{ + id: 1, + name: stoppedThreadName + }, { + id: runningThreadId, + name: runningThreadName + }], + stoppedDetails: { + reason: stoppedReason, + threadId: 1, + allThreadsStopped: false + } + }); + + const stoppedThread = session.getThread(stoppedThreadId)!; + const runningThread = session.getThread(runningThreadId)!; + + // the callstack for the stopped thread is obtainable but not available + // the callstack for the running thread is not obtainable nor available + assert.equal(stoppedThread.name, stoppedThreadName); + assert.equal(stoppedThread.stopped, true); + assert.equal(session.getAllThreads().length, 2); + assert.equal(stoppedThread.getCallStack().length, 0); + assert.equal(stoppedThread.stoppedDetails!.reason, stoppedReason); + assert.equal(runningThread.name, runningThreadName); + assert.equal(runningThread.stopped, false); + assert.equal(runningThread.getCallStack().length, 0); + assert.equal(runningThread.stoppedDetails, undefined); + + // after calling getCallStack, the callstack becomes available + // and results in a request for the callstack in the debug adapter + stoppedThread.fetchCallStack().then(() => { + assert.notEqual(stoppedThread.getCallStack().length, 0); + assert.equal(runningThread.getCallStack().length, 0); + assert.equal(sessionStub.callCount, 1); + }); + + // calling getCallStack on the running thread returns empty array + // and does not return in a request for the callstack in the debug + // adapter + runningThread.fetchCallStack().then(() => { + assert.equal(runningThread.getCallStack().length, 0); + assert.equal(sessionStub.callCount, 1); + }); + + // clearing the callstack results in the callstack not being available + stoppedThread.clearCallStack(); + assert.equal(stoppedThread.stopped, true); + assert.equal(stoppedThread.getCallStack().length, 0); + + model.clearThreads(session.getId(), true); + assert.equal(session.getThread(stoppedThreadId), undefined); + assert.equal(session.getThread(runningThreadId), undefined); + assert.equal(session.getAllThreads().length, 0); + }); + + test('stack frame get specific source name', () => { + const session = createMockSession(model); + model.addSession(session); + const { firstStackFrame, secondStackFrame } = createTwoStackFrames(session); + + assert.equal(firstStackFrame.getSpecificSourceName(), '.../b/c/d/internalModule.js'); + assert.equal(secondStackFrame.getSpecificSourceName(), '.../x/c/d/internalModule.js'); + }); + + test('stack frame toString()', () => { + const session = createMockSession(model); + const thread = new Thread(session, 'mockthread', 1); + const firstSource = new Source({ + name: 'internalModule.js', + path: 'a/b/c/d/internalModule.js', + sourceReference: 10, + }, 'aDebugSessionId'); + const stackFrame = new StackFrame(thread, 1, firstSource, 'app', 'normal', { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 10 }, 1); + assert.equal(stackFrame.toString(), 'app (internalModule.js:1)'); + + const secondSource = new Source(undefined, 'aDebugSessionId'); + const stackFrame2 = new StackFrame(thread, 2, secondSource, 'module', 'normal', { startLineNumber: undefined!, startColumn: undefined!, endLineNumber: undefined!, endColumn: undefined! }, 2); + assert.equal(stackFrame2.toString(), 'module'); + }); + + test('debug child sessions are added in correct order', () => { + const session = createMockSession(model); + model.addSession(session); + const secondSession = createMockSession(model, 'mockSession2'); + model.addSession(secondSession); + const firstChild = createMockSession(model, 'firstChild', { parentSession: session }); + model.addSession(firstChild); + const secondChild = createMockSession(model, 'secondChild', { parentSession: session }); + model.addSession(secondChild); + const thirdSession = createMockSession(model, 'mockSession3'); + model.addSession(thirdSession); + const anotherChild = createMockSession(model, 'secondChild', { parentSession: secondSession }); + model.addSession(anotherChild); + + const sessions = model.getSessions(); + assert.equal(sessions[0].getId(), session.getId()); + assert.equal(sessions[1].getId(), firstChild.getId()); + assert.equal(sessions[2].getId(), secondChild.getId()); + assert.equal(sessions[3].getId(), secondSession.getId()); + assert.equal(sessions[4].getId(), anotherChild.getId()); + assert.equal(sessions[5].getId(), thirdSession.getId()); + }); + + test('decorations', () => { + const session = createMockSession(model); + model.addSession(session); + const { firstStackFrame, secondStackFrame } = createTwoStackFrames(session); + let decorations = createDecorationsForStackFrame(firstStackFrame, firstStackFrame.range); + assert.equal(decorations.length, 2); + assert.deepEqual(decorations[0].range, new Range(1, 2, 1, 1)); + assert.equal(decorations[0].options.glyphMarginClassName, 'codicon-debug-stackframe'); + assert.deepEqual(decorations[1].range, new Range(1, Constants.MAX_SAFE_SMALL_INTEGER, 1, 1)); + assert.equal(decorations[1].options.className, 'debug-top-stack-frame-line'); + assert.equal(decorations[1].options.isWholeLine, true); + + decorations = createDecorationsForStackFrame(secondStackFrame, firstStackFrame.range); + assert.equal(decorations.length, 2); + assert.deepEqual(decorations[0].range, new Range(1, 2, 1, 1)); + assert.equal(decorations[0].options.glyphMarginClassName, 'codicon-debug-stackframe-focused'); + assert.deepEqual(decorations[1].range, new Range(1, Constants.MAX_SAFE_SMALL_INTEGER, 1, 1)); + assert.equal(decorations[1].options.className, 'debug-focused-stack-frame-line'); + assert.equal(decorations[1].options.isWholeLine, true); + + decorations = createDecorationsForStackFrame(firstStackFrame, new Range(1, 5, 1, 6)); + assert.equal(decorations.length, 3); + assert.deepEqual(decorations[0].range, new Range(1, 2, 1, 1)); + assert.equal(decorations[0].options.glyphMarginClassName, 'codicon-debug-stackframe'); + assert.deepEqual(decorations[1].range, new Range(1, Constants.MAX_SAFE_SMALL_INTEGER, 1, 1)); + assert.equal(decorations[1].options.className, 'debug-top-stack-frame-line'); + assert.equal(decorations[1].options.isWholeLine, true); + // Inline decoration gets rendered in this case + assert.equal(decorations[2].options.beforeContentClassName, 'debug-top-stack-frame-column'); + assert.deepEqual(decorations[2].range, new Range(1, Constants.MAX_SAFE_SMALL_INTEGER, 1, 1)); + }); + + test('contexts', () => { + const session = createMockSession(model); + model.addSession(session); + const { firstStackFrame, secondStackFrame } = createTwoStackFrames(session); + let context = getContext(firstStackFrame); + assert.equal(context.sessionId, firstStackFrame.thread.session.getId()); + assert.equal(context.threadId, firstStackFrame.thread.getId()); + assert.equal(context.frameId, firstStackFrame.getId()); + + context = getContext(secondStackFrame.thread); + assert.equal(context.sessionId, secondStackFrame.thread.session.getId()); + assert.equal(context.threadId, secondStackFrame.thread.getId()); + assert.equal(context.frameId, undefined); + + context = getContext(session); + assert.equal(context.sessionId, session.getId()); + assert.equal(context.threadId, undefined); + assert.equal(context.frameId, undefined); + + let contributedContext = getContextForContributedActions(firstStackFrame); + assert.equal(contributedContext, firstStackFrame.source.raw.path); + contributedContext = getContextForContributedActions(firstStackFrame.thread); + assert.equal(contributedContext, firstStackFrame.thread.threadId); + contributedContext = getContextForContributedActions(session); + assert.equal(contributedContext, session.getId()); + }); + + test('focusStackFrameThreadAndSesion', () => { + const threadId1 = 1; + const threadName1 = 'firstThread'; + const threadId2 = 2; + const threadName2 = 'secondThread'; + const stoppedReason = 'breakpoint'; + + // Add the threads + const session = new class extends DebugSession { + get state(): State { + return State.Stopped; + } + }({ resolved: { name: 'stoppedSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!); + + const runningSession = createMockSession(model); + model.addSession(runningSession); + model.addSession(session); + + session['raw'] = rawSession; + + model.rawUpdate({ + sessionId: session.getId(), + threads: [{ + id: threadId1, + name: threadName1 + }] + }); + + // Stopped event with all threads stopped + model.rawUpdate({ + sessionId: session.getId(), + threads: [{ + id: threadId1, + name: threadName1 + }, { + id: threadId2, + name: threadName2 + }], + stoppedDetails: { + reason: stoppedReason, + threadId: 1, + allThreadsStopped: true + }, + }); + + const thread = session.getThread(threadId1)!; + const runningThread = session.getThread(threadId2); + + let toFocus = getStackFrameThreadAndSessionToFocus(model, undefined); + // Verify stopped session and stopped thread get focused + assert.deepEqual(toFocus, { stackFrame: undefined, thread: thread, session: session }); + + toFocus = getStackFrameThreadAndSessionToFocus(model, undefined, undefined, runningSession); + assert.deepEqual(toFocus, { stackFrame: undefined, thread: undefined, session: runningSession }); + + toFocus = getStackFrameThreadAndSessionToFocus(model, undefined, thread); + assert.deepEqual(toFocus, { stackFrame: undefined, thread: thread, session: session }); + + toFocus = getStackFrameThreadAndSessionToFocus(model, undefined, runningThread); + assert.deepEqual(toFocus, { stackFrame: undefined, thread: runningThread, session: session }); + + const stackFrame = new StackFrame(thread, 5, undefined!, 'stackframename2', undefined, undefined!, 1); + toFocus = getStackFrameThreadAndSessionToFocus(model, stackFrame); + assert.deepEqual(toFocus, { stackFrame: stackFrame, thread: thread, session: session }); + }); +}); diff --git a/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts index 15dfe3bcbc..2115eeef4d 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts @@ -30,7 +30,7 @@ suite.skip('Debug - ANSI Handling', () => { */ setup(() => { model = new DebugModel([], [], [], [], [], { isDirty: (e: any) => false }); - session = new DebugSession({ resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService); + session = new DebugSession({ resolved: { name: 'test', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!); const instantiationService: TestInstantiationService = workbenchInstantiationService(); linkDetector = instantiationService.createInstance(LinkDetector); diff --git a/src/vs/workbench/contrib/debug/test/browser/debugHover.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugHover.test.ts new file mode 100644 index 0000000000..e529a04189 --- /dev/null +++ b/src/vs/workbench/contrib/debug/test/browser/debugHover.test.ts @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { findExpressionInStackFrame } from 'vs/workbench/contrib/debug/browser/debugHover'; +import { createMockSession } from 'vs/workbench/contrib/debug/test/browser/callStack.test'; +import { StackFrame, Thread, DebugModel, Scope, Variable } from 'vs/workbench/contrib/debug/common/debugModel'; +import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; +import type { IScope, IExpression } from 'vs/workbench/contrib/debug/common/debug'; + +suite('Debug - Hover', () => { + test('find expression in stack frame', async () => { + const model = new DebugModel([], [], [], [], [], { isDirty: (e: any) => false }); + const session = createMockSession(model); + let stackFrame: StackFrame; + + const thread = new class extends Thread { + public getCallStack(): StackFrame[] { + return [stackFrame]; + } + }(session, 'mockthread', 1); + + const firstSource = new Source({ + name: 'internalModule.js', + path: 'a/b/c/d/internalModule.js', + sourceReference: 10, + }, 'aDebugSessionId'); + + let scope: Scope; + stackFrame = new class extends StackFrame { + getScopes(): Promise { + return Promise.resolve([scope]); + } + }(thread, 1, firstSource, 'app.js', 'normal', { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 10 }, 1); + + + let variableA: Variable; + let variableB: Variable; + scope = new class extends Scope { + getChildren(): Promise { + return Promise.resolve([variableA]); + } + }(stackFrame, 1, 'local', 1, false, 10, 10); + + variableA = new class extends Variable { + getChildren(): Promise { + return Promise.resolve([variableB]); + } + }(session, 1, scope, 2, 'A', 'A', undefined!, 0, 0, {}, 'string'); + variableB = new Variable(session, 1, scope, 2, 'B', 'A.B', undefined!, 0, 0, {}, 'string'); + + assert.equal(await findExpressionInStackFrame(stackFrame, []), undefined); + assert.equal(await findExpressionInStackFrame(stackFrame, ['A']), variableA); + assert.equal(await findExpressionInStackFrame(stackFrame, ['doesNotExist', 'no']), undefined); + assert.equal(await findExpressionInStackFrame(stackFrame, ['a']), undefined); + assert.equal(await findExpressionInStackFrame(stackFrame, ['B']), undefined); + assert.equal(await findExpressionInStackFrame(stackFrame, ['A', 'B']), variableB); + assert.equal(await findExpressionInStackFrame(stackFrame, ['A', 'C']), undefined); + + // We do not search in expensive scopes + scope.expensive = true; + assert.equal(await findExpressionInStackFrame(stackFrame, ['A']), undefined); + }); +}); diff --git a/src/vs/workbench/contrib/debug/test/browser/debugModel.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugModel.test.ts deleted file mode 100644 index f7d2239d1b..0000000000 --- a/src/vs/workbench/contrib/debug/test/browser/debugModel.test.ts +++ /dev/null @@ -1,575 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import { URI as uri } from 'vs/base/common/uri'; -import severity from 'vs/base/common/severity'; -import { DebugModel, Expression, StackFrame, Thread } from 'vs/workbench/contrib/debug/common/debugModel'; -import * as sinon from 'sinon'; -import { MockRawSession, MockDebugAdapter } from 'vs/workbench/contrib/debug/test/common/mockDebug'; -import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; -import { DebugSession } from 'vs/workbench/contrib/debug/browser/debugSession'; -import { SimpleReplElement, RawObjectReplElement, ReplEvaluationInput, ReplModel, ReplEvaluationResult } from 'vs/workbench/contrib/debug/common/replModel'; -import { IBreakpointUpdateData, IDebugSessionOptions } from 'vs/workbench/contrib/debug/common/debug'; -import { NullOpenerService } from 'vs/platform/opener/common/opener'; -import { RawDebugSession } from 'vs/workbench/contrib/debug/browser/rawDebugSession'; -import { timeout } from 'vs/base/common/async'; - -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); -} - -suite('Debug - Model', () => { - let model: DebugModel; - let rawSession: MockRawSession; - - setup(() => { - model = new DebugModel([], [], [], [], [], { isDirty: (e: any) => false }); - rawSession = new MockRawSession(); - }); - - // Breakpoints - - test('breakpoints simple', () => { - const modelUri = uri.file('/myfolder/myfile.js'); - model.addBreakpoints(modelUri, [{ lineNumber: 5, enabled: true }, { lineNumber: 10, enabled: false }]); - assert.equal(model.areBreakpointsActivated(), true); - assert.equal(model.getBreakpoints().length, 2); - - model.removeBreakpoints(model.getBreakpoints()); - assert.equal(model.getBreakpoints().length, 0); - }); - - test('breakpoints toggling', () => { - const modelUri = uri.file('/myfolder/myfile.js'); - model.addBreakpoints(modelUri, [{ lineNumber: 5, enabled: true }, { lineNumber: 10, enabled: false }]); - model.addBreakpoints(modelUri, [{ lineNumber: 12, enabled: true, condition: 'fake condition' }]); - assert.equal(model.getBreakpoints().length, 3); - const bp = model.getBreakpoints().pop(); - if (bp) { - model.removeBreakpoints([bp]); - } - assert.equal(model.getBreakpoints().length, 2); - - model.setBreakpointsActivated(false); - assert.equal(model.areBreakpointsActivated(), false); - model.setBreakpointsActivated(true); - assert.equal(model.areBreakpointsActivated(), true); - }); - - test('breakpoints two files', () => { - const modelUri1 = uri.file('/myfolder/my file first.js'); - const modelUri2 = uri.file('/secondfolder/second/second file.js'); - model.addBreakpoints(modelUri1, [{ lineNumber: 5, enabled: true }, { lineNumber: 10, enabled: false }]); - model.addBreakpoints(modelUri2, [{ lineNumber: 1, enabled: true }, { lineNumber: 2, enabled: true }, { lineNumber: 3, enabled: false }]); - - assert.equal(model.getBreakpoints().length, 5); - const bp = model.getBreakpoints()[0]; - const update = new Map(); - update.set(bp.getId(), { lineNumber: 100 }); - model.updateBreakpoints(update); - assert.equal(bp.lineNumber, 100); - - model.enableOrDisableAllBreakpoints(false); - model.getBreakpoints().forEach(bp => { - assert.equal(bp.enabled, false); - }); - model.setEnablement(bp, true); - assert.equal(bp.enabled, true); - - model.removeBreakpoints(model.getBreakpoints({ uri: modelUri1 })); - assert.equal(model.getBreakpoints().length, 3); - }); - - test('breakpoints conditions', () => { - const modelUri1 = uri.file('/myfolder/my file first.js'); - model.addBreakpoints(modelUri1, [{ lineNumber: 5, condition: 'i < 5', hitCondition: '17' }, { lineNumber: 10, condition: 'j < 3' }]); - const breakpoints = model.getBreakpoints(); - - assert.equal(breakpoints[0].condition, 'i < 5'); - assert.equal(breakpoints[0].hitCondition, '17'); - assert.equal(breakpoints[1].condition, 'j < 3'); - assert.equal(!!breakpoints[1].hitCondition, false); - - assert.equal(model.getBreakpoints().length, 2); - model.removeBreakpoints(model.getBreakpoints()); - assert.equal(model.getBreakpoints().length, 0); - }); - - test('function breakpoints', () => { - model.addFunctionBreakpoint('foo', '1'); - model.addFunctionBreakpoint('bar', '2'); - model.renameFunctionBreakpoint('1', 'fooUpdated'); - model.renameFunctionBreakpoint('2', 'barUpdated'); - - const functionBps = model.getFunctionBreakpoints(); - assert.equal(functionBps[0].name, 'fooUpdated'); - assert.equal(functionBps[1].name, 'barUpdated'); - - model.removeFunctionBreakpoints(); - assert.equal(model.getFunctionBreakpoints().length, 0); - }); - - test('breakpoints multiple sessions', () => { - const modelUri = uri.file('/myfolder/myfile.js'); - const breakpoints = model.addBreakpoints(modelUri, [{ lineNumber: 5, enabled: true, condition: 'x > 5' }, { lineNumber: 10, enabled: false }]); - const session = createMockSession(model); - const data = new Map(); - - assert.equal(breakpoints[0].lineNumber, 5); - assert.equal(breakpoints[1].lineNumber, 10); - - data.set(breakpoints[0].getId(), { verified: false, line: 10 }); - data.set(breakpoints[1].getId(), { verified: true, line: 50 }); - model.setBreakpointSessionData(session.getId(), {}, data); - assert.equal(breakpoints[0].lineNumber, 5); - assert.equal(breakpoints[1].lineNumber, 50); - - const session2 = createMockSession(model); - const data2 = new Map(); - data2.set(breakpoints[0].getId(), { verified: true, line: 100 }); - data2.set(breakpoints[1].getId(), { verified: true, line: 500 }); - model.setBreakpointSessionData(session2.getId(), {}, data2); - - // Breakpoint is verified only once, show that line - assert.equal(breakpoints[0].lineNumber, 100); - // Breakpoint is verified two times, show the original line - assert.equal(breakpoints[1].lineNumber, 10); - - model.setBreakpointSessionData(session.getId(), {}, undefined); - // No more double session verification - assert.equal(breakpoints[0].lineNumber, 100); - assert.equal(breakpoints[1].lineNumber, 500); - - assert.equal(breakpoints[0].supported, false); - const data3 = new Map(); - data3.set(breakpoints[0].getId(), { verified: true, line: 500 }); - model.setBreakpointSessionData(session2.getId(), { supportsConditionalBreakpoints: true }, data2); - assert.equal(breakpoints[0].supported, true); - }); - - // Threads - - test('threads simple', () => { - const threadId = 1; - const threadName = 'firstThread'; - const session = createMockSession(model); - model.addSession(session); - - assert.equal(model.getSessions(true).length, 1); - model.rawUpdate({ - sessionId: session.getId(), - threads: [{ - id: threadId, - name: threadName - }] - }); - - assert.equal(session.getThread(threadId)!.name, threadName); - - model.clearThreads(session.getId(), true); - assert.equal(session.getThread(threadId), undefined); - assert.equal(model.getSessions(true).length, 1); - }); - - test('threads multiple wtih allThreadsStopped', () => { - const threadId1 = 1; - const threadName1 = 'firstThread'; - const threadId2 = 2; - const threadName2 = 'secondThread'; - const stoppedReason = 'breakpoint'; - - // Add the threads - const session = createMockSession(model); - model.addSession(session); - - session['raw'] = rawSession; - - model.rawUpdate({ - sessionId: session.getId(), - threads: [{ - id: threadId1, - name: threadName1 - }] - }); - - // Stopped event with all threads stopped - model.rawUpdate({ - sessionId: session.getId(), - threads: [{ - id: threadId1, - name: threadName1 - }, { - id: threadId2, - name: threadName2 - }], - stoppedDetails: { - reason: stoppedReason, - threadId: 1, - allThreadsStopped: true - }, - }); - - const thread1 = session.getThread(threadId1)!; - const thread2 = session.getThread(threadId2)!; - - // at the beginning, callstacks are obtainable but not available - assert.equal(session.getAllThreads().length, 2); - assert.equal(thread1.name, threadName1); - assert.equal(thread1.stopped, true); - assert.equal(thread1.getCallStack().length, 0); - assert.equal(thread1.stoppedDetails!.reason, stoppedReason); - assert.equal(thread2.name, threadName2); - assert.equal(thread2.stopped, true); - assert.equal(thread2.getCallStack().length, 0); - assert.equal(thread2.stoppedDetails!.reason, undefined); - - // after calling getCallStack, the callstack becomes available - // and results in a request for the callstack in the debug adapter - thread1.fetchCallStack().then(() => { - assert.notEqual(thread1.getCallStack().length, 0); - }); - - thread2.fetchCallStack().then(() => { - assert.notEqual(thread2.getCallStack().length, 0); - }); - - // calling multiple times getCallStack doesn't result in multiple calls - // to the debug adapter - thread1.fetchCallStack().then(() => { - return thread2.fetchCallStack(); - }); - - // clearing the callstack results in the callstack not being available - thread1.clearCallStack(); - assert.equal(thread1.stopped, true); - assert.equal(thread1.getCallStack().length, 0); - - thread2.clearCallStack(); - assert.equal(thread2.stopped, true); - assert.equal(thread2.getCallStack().length, 0); - - model.clearThreads(session.getId(), true); - assert.equal(session.getThread(threadId1), undefined); - assert.equal(session.getThread(threadId2), undefined); - assert.equal(session.getAllThreads().length, 0); - }); - - test('threads mutltiple without allThreadsStopped', () => { - const sessionStub = sinon.spy(rawSession, 'stackTrace'); - - const stoppedThreadId = 1; - const stoppedThreadName = 'stoppedThread'; - const runningThreadId = 2; - const runningThreadName = 'runningThread'; - const stoppedReason = 'breakpoint'; - const session = createMockSession(model); - model.addSession(session); - - session['raw'] = rawSession; - - // Add the threads - model.rawUpdate({ - sessionId: session.getId(), - threads: [{ - id: stoppedThreadId, - name: stoppedThreadName - }] - }); - - // Stopped event with only one thread stopped - model.rawUpdate({ - sessionId: session.getId(), - threads: [{ - id: 1, - name: stoppedThreadName - }, { - id: runningThreadId, - name: runningThreadName - }], - stoppedDetails: { - reason: stoppedReason, - threadId: 1, - allThreadsStopped: false - } - }); - - const stoppedThread = session.getThread(stoppedThreadId)!; - const runningThread = session.getThread(runningThreadId)!; - - // the callstack for the stopped thread is obtainable but not available - // the callstack for the running thread is not obtainable nor available - assert.equal(stoppedThread.name, stoppedThreadName); - assert.equal(stoppedThread.stopped, true); - assert.equal(session.getAllThreads().length, 2); - assert.equal(stoppedThread.getCallStack().length, 0); - assert.equal(stoppedThread.stoppedDetails!.reason, stoppedReason); - assert.equal(runningThread.name, runningThreadName); - assert.equal(runningThread.stopped, false); - assert.equal(runningThread.getCallStack().length, 0); - assert.equal(runningThread.stoppedDetails, undefined); - - // after calling getCallStack, the callstack becomes available - // and results in a request for the callstack in the debug adapter - stoppedThread.fetchCallStack().then(() => { - assert.notEqual(stoppedThread.getCallStack().length, 0); - assert.equal(runningThread.getCallStack().length, 0); - assert.equal(sessionStub.callCount, 1); - }); - - // calling getCallStack on the running thread returns empty array - // and does not return in a request for the callstack in the debug - // adapter - runningThread.fetchCallStack().then(() => { - assert.equal(runningThread.getCallStack().length, 0); - assert.equal(sessionStub.callCount, 1); - }); - - // clearing the callstack results in the callstack not being available - stoppedThread.clearCallStack(); - assert.equal(stoppedThread.stopped, true); - assert.equal(stoppedThread.getCallStack().length, 0); - - model.clearThreads(session.getId(), true); - assert.equal(session.getThread(stoppedThreadId), undefined); - assert.equal(session.getThread(runningThreadId), undefined); - assert.equal(session.getAllThreads().length, 0); - }); - - // Expressions - - function assertWatchExpressions(watchExpressions: Expression[], expectedName: string) { - assert.equal(watchExpressions.length, 2); - watchExpressions.forEach(we => { - assert.equal(we.available, false); - assert.equal(we.reference, 0); - assert.equal(we.name, expectedName); - }); - } - - test('watch expressions', () => { - assert.equal(model.getWatchExpressions().length, 0); - model.addWatchExpression('console'); - model.addWatchExpression('console'); - let watchExpressions = model.getWatchExpressions(); - assertWatchExpressions(watchExpressions, 'console'); - - model.renameWatchExpression(watchExpressions[0].getId(), 'new_name'); - model.renameWatchExpression(watchExpressions[1].getId(), 'new_name'); - assertWatchExpressions(model.getWatchExpressions(), 'new_name'); - - assertWatchExpressions(model.getWatchExpressions(), 'new_name'); - - model.addWatchExpression('mockExpression'); - model.moveWatchExpression(model.getWatchExpressions()[2].getId(), 1); - watchExpressions = model.getWatchExpressions(); - assert.equal(watchExpressions[0].name, 'new_name'); - assert.equal(watchExpressions[1].name, 'mockExpression'); - assert.equal(watchExpressions[2].name, 'new_name'); - - model.removeWatchExpressions(); - assert.equal(model.getWatchExpressions().length, 0); - }); - - test('repl expressions', () => { - const session = createMockSession(model); - assert.equal(session.getReplElements().length, 0); - model.addSession(session); - - session['raw'] = rawSession; - const thread = new Thread(session, 'mockthread', 1); - const stackFrame = new StackFrame(thread, 1, undefined, 'app.js', 'normal', { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 10 }, 1); - const replModel = new ReplModel(); - replModel.addReplExpression(session, stackFrame, 'myVariable').then(); - replModel.addReplExpression(session, stackFrame, 'myVariable').then(); - replModel.addReplExpression(session, stackFrame, 'myVariable').then(); - - assert.equal(replModel.getReplElements().length, 3); - replModel.getReplElements().forEach(re => { - assert.equal((re).value, 'myVariable'); - }); - - replModel.removeReplExpressions(); - assert.equal(replModel.getReplElements().length, 0); - }); - - test('stack frame get specific source name', () => { - const session = createMockSession(model); - model.addSession(session); - - let firstStackFrame: StackFrame; - let secondStackFrame: StackFrame; - const thread = new class extends Thread { - public getCallStack(): StackFrame[] { - return [firstStackFrame, secondStackFrame]; - } - }(session, 'mockthread', 1); - - const firstSource = new Source({ - name: 'internalModule.js', - path: 'a/b/c/d/internalModule.js', - sourceReference: 10, - }, 'aDebugSessionId'); - const secondSource = new Source({ - name: 'internalModule.js', - path: 'z/x/c/d/internalModule.js', - sourceReference: 11, - }, 'aDebugSessionId'); - firstStackFrame = new StackFrame(thread, 1, firstSource, 'app.js', 'normal', { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 10 }, 1); - secondStackFrame = new StackFrame(thread, 1, secondSource, 'app.js', 'normal', { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 10 }, 1); - - assert.equal(firstStackFrame.getSpecificSourceName(), '.../b/c/d/internalModule.js'); - assert.equal(secondStackFrame.getSpecificSourceName(), '.../x/c/d/internalModule.js'); - }); - - test('stack frame toString()', () => { - const session = createMockSession(model); - const thread = new Thread(session, 'mockthread', 1); - const firstSource = new Source({ - name: 'internalModule.js', - path: 'a/b/c/d/internalModule.js', - sourceReference: 10, - }, 'aDebugSessionId'); - const stackFrame = new StackFrame(thread, 1, firstSource, 'app', 'normal', { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 10 }, 1); - assert.equal(stackFrame.toString(), 'app (internalModule.js:1)'); - - const secondSource = new Source(undefined, 'aDebugSessionId'); - const stackFrame2 = new StackFrame(thread, 2, secondSource, 'module', 'normal', { startLineNumber: undefined!, startColumn: undefined!, endLineNumber: undefined!, endColumn: undefined! }, 2); - assert.equal(stackFrame2.toString(), 'module'); - }); - - test('debug child sessions are added in correct order', () => { - const session = createMockSession(model); - model.addSession(session); - const secondSession = createMockSession(model, 'mockSession2'); - model.addSession(secondSession); - const firstChild = createMockSession(model, 'firstChild', { parentSession: session }); - model.addSession(firstChild); - const secondChild = createMockSession(model, 'secondChild', { parentSession: session }); - model.addSession(secondChild); - const thirdSession = createMockSession(model, 'mockSession3'); - model.addSession(thirdSession); - const anotherChild = createMockSession(model, 'secondChild', { parentSession: secondSession }); - model.addSession(anotherChild); - - const sessions = model.getSessions(); - assert.equal(sessions[0].getId(), session.getId()); - assert.equal(sessions[1].getId(), firstChild.getId()); - assert.equal(sessions[2].getId(), secondChild.getId()); - assert.equal(sessions[3].getId(), secondSession.getId()); - assert.equal(sessions[4].getId(), anotherChild.getId()); - assert.equal(sessions[5].getId(), thirdSession.getId()); - }); - - // Repl output - - test('repl output', () => { - const session = createMockSession(model); - const repl = new ReplModel(); - repl.appendToRepl(session, 'first line\n', severity.Error); - repl.appendToRepl(session, 'second line ', severity.Error); - repl.appendToRepl(session, 'third line ', severity.Error); - repl.appendToRepl(session, 'fourth line', severity.Error); - - let elements = repl.getReplElements(); - assert.equal(elements.length, 2); - assert.equal(elements[0].value, 'first line\n'); - assert.equal(elements[0].severity, severity.Error); - assert.equal(elements[1].value, 'second line third line fourth line'); - assert.equal(elements[1].severity, severity.Error); - - repl.appendToRepl(session, '1', severity.Warning); - elements = repl.getReplElements(); - assert.equal(elements.length, 3); - assert.equal(elements[2].value, '1'); - assert.equal(elements[2].severity, severity.Warning); - - const keyValueObject = { 'key1': 2, 'key2': 'value' }; - repl.appendToRepl(session, new RawObjectReplElement('fakeid', 'fake', keyValueObject), severity.Info); - const element = repl.getReplElements()[3]; - assert.equal(element.value, 'Object'); - assert.deepEqual(element.valueObj, keyValueObject); - - repl.removeReplExpressions(); - assert.equal(repl.getReplElements().length, 0); - - repl.appendToRepl(session, '1\n', severity.Info); - repl.appendToRepl(session, '2', severity.Info); - repl.appendToRepl(session, '3\n4', severity.Info); - repl.appendToRepl(session, '5\n', severity.Info); - repl.appendToRepl(session, '6', severity.Info); - elements = repl.getReplElements(); - assert.equal(elements.length, 3); - assert.equal(elements[0], '1\n'); - assert.equal(elements[1], '23\n45\n'); - assert.equal(elements[2], '6'); - }); - - test('repl merging', () => { - // 'mergeWithParent' should be ignored when there is no parent. - const parent = createMockSession(model, 'parent', { repl: 'mergeWithParent' }); - const child1 = createMockSession(model, 'child1', { parentSession: parent, repl: 'separate' }); - const child2 = createMockSession(model, 'child2', { parentSession: parent, repl: 'mergeWithParent' }); - const grandChild = createMockSession(model, 'grandChild', { parentSession: child2, repl: 'mergeWithParent' }); - const child3 = createMockSession(model, 'child3', { parentSession: parent }); - - let parentChanges = 0; - parent.onDidChangeReplElements(() => ++parentChanges); - - parent.appendToRepl('1\n', severity.Info); - assert.equal(parentChanges, 1); - assert.equal(parent.getReplElements().length, 1); - assert.equal(child1.getReplElements().length, 0); - assert.equal(child2.getReplElements().length, 1); - assert.equal(grandChild.getReplElements().length, 1); - assert.equal(child3.getReplElements().length, 0); - - grandChild.appendToRepl('1\n', severity.Info); - assert.equal(parentChanges, 2); - assert.equal(parent.getReplElements().length, 2); - assert.equal(child1.getReplElements().length, 0); - assert.equal(child2.getReplElements().length, 2); - assert.equal(grandChild.getReplElements().length, 2); - assert.equal(child3.getReplElements().length, 0); - - child3.appendToRepl('1\n', severity.Info); - assert.equal(parentChanges, 2); - assert.equal(parent.getReplElements().length, 2); - assert.equal(child1.getReplElements().length, 0); - assert.equal(child2.getReplElements().length, 2); - assert.equal(grandChild.getReplElements().length, 2); - assert.equal(child3.getReplElements().length, 1); - - child1.appendToRepl('1\n', severity.Info); - assert.equal(parentChanges, 2); - assert.equal(parent.getReplElements().length, 2); - assert.equal(child1.getReplElements().length, 1); - assert.equal(child2.getReplElements().length, 2); - assert.equal(grandChild.getReplElements().length, 2); - assert.equal(child3.getReplElements().length, 1); - }); - - test('repl ordering', async () => { - const session = createMockSession(model); - model.addSession(session); - - const adapter = new MockDebugAdapter(); - const raw = new RawDebugSession(adapter, undefined!, undefined!, undefined!, undefined!, undefined!); - session.initializeForTest(raw); - - await session.addReplExpression(undefined, 'before.1'); - assert.equal(session.getReplElements().length, 3); - assert.equal((session.getReplElements()[0]).value, 'before.1'); - assert.equal((session.getReplElements()[1]).value, 'before.1'); - assert.equal((session.getReplElements()[2]).value, '=before.1'); - - await session.addReplExpression(undefined, 'after.2'); - await timeout(0); - assert.equal(session.getReplElements().length, 6); - assert.equal((session.getReplElements()[3]).value, 'after.2'); - assert.equal((session.getReplElements()[4]).value, '=after.2'); - assert.equal((session.getReplElements()[5]).value, 'after.2'); - }); -}); diff --git a/src/vs/workbench/contrib/debug/test/browser/repl.test.ts b/src/vs/workbench/contrib/debug/test/browser/repl.test.ts new file mode 100644 index 0000000000..35ec93e381 --- /dev/null +++ b/src/vs/workbench/contrib/debug/test/browser/repl.test.ts @@ -0,0 +1,154 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import * as assert from 'assert'; +import severity from 'vs/base/common/severity'; +import { DebugModel, StackFrame, Thread } from 'vs/workbench/contrib/debug/common/debugModel'; +import { MockRawSession, MockDebugAdapter } from 'vs/workbench/contrib/debug/test/common/mockDebug'; +import { SimpleReplElement, RawObjectReplElement, ReplEvaluationInput, ReplModel, ReplEvaluationResult } from 'vs/workbench/contrib/debug/common/replModel'; +import { RawDebugSession } from 'vs/workbench/contrib/debug/browser/rawDebugSession'; +import { timeout } from 'vs/base/common/async'; +import { createMockSession } from 'vs/workbench/contrib/debug/test/browser/callStack.test'; + +suite('Debug - REPL', () => { + let model: DebugModel; + let rawSession: MockRawSession; + + setup(() => { + model = new DebugModel([], [], [], [], [], { isDirty: (e: any) => false }); + rawSession = new MockRawSession(); + }); + + test('repl output', () => { + const session = createMockSession(model); + const repl = new ReplModel(); + repl.appendToRepl(session, 'first line\n', severity.Error); + repl.appendToRepl(session, 'second line ', severity.Error); + repl.appendToRepl(session, 'third line ', severity.Error); + repl.appendToRepl(session, 'fourth line', severity.Error); + + let elements = repl.getReplElements(); + assert.equal(elements.length, 2); + assert.equal(elements[0].value, 'first line\n'); + assert.equal(elements[0].severity, severity.Error); + assert.equal(elements[1].value, 'second line third line fourth line'); + assert.equal(elements[1].severity, severity.Error); + + repl.appendToRepl(session, '1', severity.Warning); + elements = repl.getReplElements(); + assert.equal(elements.length, 3); + assert.equal(elements[2].value, '1'); + assert.equal(elements[2].severity, severity.Warning); + + const keyValueObject = { 'key1': 2, 'key2': 'value' }; + repl.appendToRepl(session, new RawObjectReplElement('fakeid', 'fake', keyValueObject), severity.Info); + const element = repl.getReplElements()[3]; + assert.equal(element.value, 'Object'); + assert.deepEqual(element.valueObj, keyValueObject); + + repl.removeReplExpressions(); + assert.equal(repl.getReplElements().length, 0); + + repl.appendToRepl(session, '1\n', severity.Info); + repl.appendToRepl(session, '2', severity.Info); + repl.appendToRepl(session, '3\n4', severity.Info); + repl.appendToRepl(session, '5\n', severity.Info); + repl.appendToRepl(session, '6', severity.Info); + elements = repl.getReplElements(); + assert.equal(elements.length, 3); + assert.equal(elements[0], '1\n'); + assert.equal(elements[1], '23\n45\n'); + assert.equal(elements[2], '6'); + }); + + test('repl merging', () => { + // 'mergeWithParent' should be ignored when there is no parent. + const parent = createMockSession(model, 'parent', { repl: 'mergeWithParent' }); + const child1 = createMockSession(model, 'child1', { parentSession: parent, repl: 'separate' }); + const child2 = createMockSession(model, 'child2', { parentSession: parent, repl: 'mergeWithParent' }); + const grandChild = createMockSession(model, 'grandChild', { parentSession: child2, repl: 'mergeWithParent' }); + const child3 = createMockSession(model, 'child3', { parentSession: parent }); + + let parentChanges = 0; + parent.onDidChangeReplElements(() => ++parentChanges); + + parent.appendToRepl('1\n', severity.Info); + assert.equal(parentChanges, 1); + assert.equal(parent.getReplElements().length, 1); + assert.equal(child1.getReplElements().length, 0); + assert.equal(child2.getReplElements().length, 1); + assert.equal(grandChild.getReplElements().length, 1); + assert.equal(child3.getReplElements().length, 0); + + grandChild.appendToRepl('1\n', severity.Info); + assert.equal(parentChanges, 2); + assert.equal(parent.getReplElements().length, 2); + assert.equal(child1.getReplElements().length, 0); + assert.equal(child2.getReplElements().length, 2); + assert.equal(grandChild.getReplElements().length, 2); + assert.equal(child3.getReplElements().length, 0); + + child3.appendToRepl('1\n', severity.Info); + assert.equal(parentChanges, 2); + assert.equal(parent.getReplElements().length, 2); + assert.equal(child1.getReplElements().length, 0); + assert.equal(child2.getReplElements().length, 2); + assert.equal(grandChild.getReplElements().length, 2); + assert.equal(child3.getReplElements().length, 1); + + child1.appendToRepl('1\n', severity.Info); + assert.equal(parentChanges, 2); + assert.equal(parent.getReplElements().length, 2); + assert.equal(child1.getReplElements().length, 1); + assert.equal(child2.getReplElements().length, 2); + assert.equal(grandChild.getReplElements().length, 2); + assert.equal(child3.getReplElements().length, 1); + }); + + test('repl expressions', () => { + const session = createMockSession(model); + assert.equal(session.getReplElements().length, 0); + model.addSession(session); + + session['raw'] = rawSession; + const thread = new Thread(session, 'mockthread', 1); + const stackFrame = new StackFrame(thread, 1, undefined, 'app.js', 'normal', { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 10 }, 1); + const replModel = new ReplModel(); + replModel.addReplExpression(session, stackFrame, 'myVariable').then(); + replModel.addReplExpression(session, stackFrame, 'myVariable').then(); + replModel.addReplExpression(session, stackFrame, 'myVariable').then(); + + assert.equal(replModel.getReplElements().length, 3); + replModel.getReplElements().forEach(re => { + assert.equal((re).value, 'myVariable'); + }); + + replModel.removeReplExpressions(); + assert.equal(replModel.getReplElements().length, 0); + }); + + test('repl ordering', async () => { + const session = createMockSession(model); + model.addSession(session); + + const adapter = new MockDebugAdapter(); + const raw = new RawDebugSession(adapter, undefined!, undefined!, undefined!, undefined!, undefined!); + session.initializeForTest(raw); + + await session.addReplExpression(undefined, 'before.1'); + assert.equal(session.getReplElements().length, 3); + assert.equal((session.getReplElements()[0]).value, 'before.1'); + assert.equal((session.getReplElements()[1]).value, 'before.1'); + assert.equal((session.getReplElements()[2]).value, '=before.1'); + + await session.addReplExpression(undefined, 'after.2'); + await timeout(0); + assert.equal(session.getReplElements().length, 6); + assert.equal((session.getReplElements()[3]).value, 'after.2'); + assert.equal((session.getReplElements()[4]).value, '=after.2'); + assert.equal((session.getReplElements()[5]).value, 'after.2'); + }); +}); diff --git a/src/vs/workbench/contrib/debug/test/browser/watch.test.ts b/src/vs/workbench/contrib/debug/test/browser/watch.test.ts new file mode 100644 index 0000000000..e6ecd807dc --- /dev/null +++ b/src/vs/workbench/contrib/debug/test/browser/watch.test.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { Expression, DebugModel } from 'vs/workbench/contrib/debug/common/debugModel'; + +// Expressions + +function assertWatchExpressions(watchExpressions: Expression[], expectedName: string) { + assert.equal(watchExpressions.length, 2); + watchExpressions.forEach(we => { + assert.equal(we.available, false); + assert.equal(we.reference, 0); + assert.equal(we.name, expectedName); + }); +} + +suite('Debug - Watch', () => { + + let model: DebugModel; + + setup(() => { + model = new DebugModel([], [], [], [], [], { isDirty: (e: any) => false }); + }); + + test('watch expressions', () => { + assert.equal(model.getWatchExpressions().length, 0); + model.addWatchExpression('console'); + model.addWatchExpression('console'); + let watchExpressions = model.getWatchExpressions(); + assertWatchExpressions(watchExpressions, 'console'); + + model.renameWatchExpression(watchExpressions[0].getId(), 'new_name'); + model.renameWatchExpression(watchExpressions[1].getId(), 'new_name'); + assertWatchExpressions(model.getWatchExpressions(), 'new_name'); + + assertWatchExpressions(model.getWatchExpressions(), 'new_name'); + + model.addWatchExpression('mockExpression'); + model.moveWatchExpression(model.getWatchExpressions()[2].getId(), 1); + watchExpressions = model.getWatchExpressions(); + assert.equal(watchExpressions[0].name, 'new_name'); + assert.equal(watchExpressions[1].name, 'mockExpression'); + assert.equal(watchExpressions[2].name, 'new_name'); + + model.removeWatchExpressions(); + assert.equal(model.getWatchExpressions().length, 0); + }); +}); diff --git a/src/vs/workbench/contrib/debug/test/common/debugUtils.test.ts b/src/vs/workbench/contrib/debug/test/common/debugUtils.test.ts index e141ef7009..f6ac40a2f5 100644 --- a/src/vs/workbench/contrib/debug/test/common/debugUtils.test.ts +++ b/src/vs/workbench/contrib/debug/test/common/debugUtils.test.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { formatPII, getExactExpressionStartAndEnd } from 'vs/workbench/contrib/debug/common/debugUtils'; +import { formatPII, getExactExpressionStartAndEnd, getVisibleAndSorted } from 'vs/workbench/contrib/debug/common/debugUtils'; +import { IConfig } from 'vs/workbench/contrib/debug/common/debug'; suite('Debug - Utils', () => { test('formatPII', () => { @@ -37,4 +38,80 @@ suite('Debug - Utils', () => { assert.deepEqual(getExactExpressionStartAndEnd('var t = a.b;c.d.name', 16, 20), { start: 13, end: 20 }); assert.deepEqual(getExactExpressionStartAndEnd('var t = a.b.c-d.name', 16, 20), { start: 15, end: 20 }); }); + + test('config presentation', () => { + const configs: IConfig[] = []; + configs.push({ + type: 'node', + request: 'launch', + name: 'p' + }); + configs.push({ + type: 'node', + request: 'launch', + name: 'a' + }); + configs.push({ + type: 'node', + request: 'launch', + name: 'b', + presentation: { + hidden: false + } + }); + configs.push({ + type: 'node', + request: 'launch', + name: 'c', + presentation: { + hidden: true + } + }); + configs.push({ + type: 'node', + request: 'launch', + name: 'd', + presentation: { + group: '2_group', + order: 5 + } + }); + configs.push({ + type: 'node', + request: 'launch', + name: 'e', + presentation: { + group: '2_group', + order: 52 + } + }); + configs.push({ + type: 'node', + request: 'launch', + name: 'f', + presentation: { + group: '1_group', + order: 500 + } + }); + configs.push({ + type: 'node', + request: 'launch', + name: 'g', + presentation: { + group: '5_group', + order: 500 + } + }); + + const sorted = getVisibleAndSorted(configs); + assert.equal(sorted.length, 7); + assert.equal(sorted[0].name, 'f'); + assert.equal(sorted[1].name, 'd'); + assert.equal(sorted[2].name, 'e'); + assert.equal(sorted[3].name, 'g'); + assert.equal(sorted[4].name, 'b'); + assert.equal(sorted[5].name, 'p'); + assert.equal(sorted[6].name, 'a'); + }); }); diff --git a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts index 0b44d1c42d..1946b3b251 100644 --- a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts @@ -9,7 +9,6 @@ import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { Position, IPosition } from 'vs/editor/common/core/position'; import { ILaunch, IDebugService, State, IDebugSession, IConfigurationManager, IStackFrame, IBreakpointData, IBreakpointUpdateData, IConfig, IDebugModel, IViewModel, IBreakpoint, LoadedSourceEvent, IThread, IRawModelUpdate, IFunctionBreakpoint, IExceptionBreakpoint, IDebugger, IExceptionInfo, AdapterEndEvent, IReplElement, IExpression, IReplElementSource, IDataBreakpoint, IDebugSessionOptions } from 'vs/workbench/contrib/debug/common/debug'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; -import { CompletionItem } from 'vs/editor/common/modes'; import Severity from 'vs/base/common/severity'; import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; @@ -237,8 +236,8 @@ export class MockSession implements IDebugSession { return Promise.resolve([]); } - completions(frameId: number, text: string, position: Position, overwriteBefore: number): Promise { - return Promise.resolve([]); + completions(frameId: number, text: string, position: Position, overwriteBefore: number): Promise { + throw new Error('not implemented'); } clearThreads(removeThreads: boolean, reference?: number): void { } diff --git a/src/vs/workbench/contrib/debug/test/common/rawDebugSession.test.ts b/src/vs/workbench/contrib/debug/test/common/rawDebugSession.test.ts new file mode 100644 index 0000000000..466411e348 --- /dev/null +++ b/src/vs/workbench/contrib/debug/test/common/rawDebugSession.test.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { MockDebugAdapter } from 'vs/workbench/contrib/debug/test/common/mockDebug'; +import { timeout } from 'vs/base/common/async'; + +suite('Debug - AbstractDebugAdapter', () => { + suite('event ordering', () => { + let adapter: MockDebugAdapter; + let output: string[]; + setup(() => { + adapter = new MockDebugAdapter(); + output = []; + adapter.onEvent(ev => { + output.push((ev as DebugProtocol.OutputEvent).body.output); + Promise.resolve().then(() => output.push('--end microtask--')); + }); + }); + + const evaluate = async (expression: string) => { + await new Promise(resolve => adapter.sendRequest('evaluate', { expression }, resolve)); + output.push(`=${expression}`); + Promise.resolve().then(() => output.push('--end microtask--')); + }; + + test('inserts task boundary before response', async () => { + await evaluate('before.foo'); + await timeout(0); + + assert.deepStrictEqual(output, ['before.foo', '--end microtask--', '=before.foo', '--end microtask--']); + }); + + test('inserts task boundary after response', async () => { + await evaluate('after.foo'); + await timeout(0); + + assert.deepStrictEqual(output, ['=after.foo', '--end microtask--', 'after.foo', '--end microtask--']); + }); + + test('does not insert boundaries between events', async () => { + adapter.sendEventBody('output', { output: 'a' }); + adapter.sendEventBody('output', { output: 'b' }); + adapter.sendEventBody('output', { output: 'c' }); + await timeout(0); + + assert.deepStrictEqual(output, ['a', 'b', 'c', '--end microtask--', '--end microtask--', '--end microtask--']); + }); + }); +}); diff --git a/src/vs/workbench/contrib/experiments/common/experimentService.ts b/src/vs/workbench/contrib/experiments/common/experimentService.ts index 97d5a685d7..7680311273 100644 --- a/src/vs/workbench/contrib/experiments/common/experimentService.ts +++ b/src/vs/workbench/contrib/experiments/common/experimentService.ts @@ -14,12 +14,13 @@ import { language } from 'vs/base/common/platform'; import { Disposable } from 'vs/base/common/lifecycle'; import { match } from 'vs/base/common/glob'; import { IRequestService, asJson } from 'vs/platform/request/common/request'; -import { ITextFileService, StateChange } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileService, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; import { CancellationToken } from 'vs/base/common/cancellation'; import { distinct } from 'vs/base/common/arrays'; import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { IProductService } from 'vs/platform/product/common/productService'; import { IWorkspaceTagsService } from 'vs/workbench/contrib/tags/common/workspaceTags'; +import { RunOnceWorker } from 'vs/base/common/async'; export const enum ExperimentState { Evaluating, @@ -402,16 +403,17 @@ export class ExperimentService extends Disposable implements IExperimentService return ExperimentState.Run; } - const onSaveHandler = this.textFileService.models.onModelsSaved(e => { + // Process model-save event every 250ms to reduce load + const onModelsSavedWorker = this._register(new RunOnceWorker(models => { const date = new Date().toDateString(); const latestExperimentState: IExperimentStorageState = safeParse(this.storageService.get(storageKey, StorageScope.GLOBAL), {}); if (latestExperimentState.state !== ExperimentState.Evaluating) { onSaveHandler.dispose(); + onModelsSavedWorker.dispose(); return; } - e.forEach(async event => { - if (event.kind !== StateChange.SAVED - || latestExperimentState.state !== ExperimentState.Evaluating + models.forEach(async model => { + if (latestExperimentState.state !== ExperimentState.Evaluating || date === latestExperimentState.lastEditedDate || (typeof latestExperimentState.editCount === 'number' && latestExperimentState.editCount >= fileEdits.minEditCount) ) { @@ -421,7 +423,7 @@ export class ExperimentService extends Disposable implements IExperimentService let workspaceCheck = true; if (typeof fileEdits.filePathPattern === 'string') { - filePathCheck = match(fileEdits.filePathPattern, event.resource.fsPath); + filePathCheck = match(fileEdits.filePathPattern, model.resource.fsPath); } if (Array.isArray(fileEdits.workspaceIncludes) && fileEdits.workspaceIncludes.length) { const tags = await this.workspaceTagsService.getTags(); @@ -444,8 +446,9 @@ export class ExperimentService extends Disposable implements IExperimentService this.fireRunExperiment(processedExperiment); } } - }); - this._register(onSaveHandler); + }, 250)); + + const onSaveHandler = this._register(this.textFileService.files.onDidSave(e => onModelsSavedWorker.work(e.model))); return ExperimentState.Evaluating; }); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index a789d45eab..08b26d43e5 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -6,7 +6,7 @@ import { localize } from 'vs/nls'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; import { Registry } from 'vs/platform/registry/common/platform'; -import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuRegistry, MenuId, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ExtensionsLabel, ExtensionsChannelId, PreferencesLabel, IExtensionManagementService, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionManagementServerService, IExtensionTipsService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -44,6 +44,9 @@ import { ExtensionType, ExtensionsPolicy } from 'vs/platform/extensions/common/e import { RemoteExtensionsInstaller } from 'vs/workbench/contrib/extensions/browser/remoteExtensionsInstaller'; import { ExtensionTipsService } from 'vs/workbench/contrib/extensions/browser/extensionTipsService'; import { IViewContainersRegistry, ViewContainerLocation, Extensions as ViewContainerExtensions } from 'vs/workbench/common/views'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; // Singletons registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); @@ -79,7 +82,7 @@ Registry.as(ViewContainerExtensions.ViewContainersRegis { id: VIEWLET_ID, name: localize('extensions', "Extensions"), - ctorDescriptor: { ctor: ExtensionsViewPaneContainer }, + ctorDescriptor: new SyncDescriptor(ExtensionsViewPaneContainer), icon: 'codicon-extensions', order: 14 // {{SQL CARBON EDIT}} }, ViewContainerLocation.Sidebar); @@ -344,6 +347,75 @@ MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { order: 3 }); +// Extension Context Menu + +registerAction2(class extends Action2 { + + constructor() { + super({ + id: 'workbench.extensions.action.copyExtension', + title: { value: localize('workbench.extensions.action.copyExtension', "Copy"), original: 'Copy' }, + menu: { + id: MenuId.ExtensionContext, + group: '1_copy' + } + }); + } + + async run(accessor: ServicesAccessor, extensionId: string) { + const extensionWorkbenchService = accessor.get(IExtensionsWorkbenchService); + let extension = extensionWorkbenchService.local.filter(e => areSameExtensions(e.identifier, { id: extensionId }))[0] + || (await extensionWorkbenchService.queryGallery({ names: [extensionId], pageSize: 1 }, CancellationToken.None)).firstPage[0]; + if (extension) { + const name = localize('extensionInfoName', 'Name: {0}', extension.displayName); + const id = localize('extensionInfoId', 'Id: {0}', extensionId); + const description = localize('extensionInfoDescription', 'Description: {0}', extension.description); + const verision = localize('extensionInfoVersion', 'Version: {0}', extension.version); + const publisher = localize('extensionInfoPublisher', 'Publisher: {0}', extension.publisherDisplayName); + const link = extension.url ? localize('extensionInfoVSMarketplaceLink', 'VS Marketplace Link: {0}', `${extension.url}`) : null; + const clipboardStr = `${name}\n${id}\n${description}\n${verision}\n${publisher}${link ? '\n' + link : ''}`; + await accessor.get(IClipboardService).writeText(clipboardStr); + } + } +}); + +registerAction2(class extends Action2 { + + constructor() { + super({ + id: 'workbench.extensions.action.copyExtensionId', + title: { value: localize('workbench.extensions.action.copyExtensionId', "Copy Extension Id"), original: 'Copy Extension Id' }, + menu: { + id: MenuId.ExtensionContext, + group: '1_copy' + } + }); + } + + async run(accessor: ServicesAccessor, id: string) { + await accessor.get(IClipboardService).writeText(id); + } +}); + +registerAction2(class extends Action2 { + + constructor() { + super({ + id: 'workbench.extensions.action.configure', + title: { value: localize('workbench.extensions.action.configure', "Configure..."), original: 'Configure...' }, + menu: { + id: MenuId.ExtensionContext, + group: '2_configure', + when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'installed'), ContextKeyExpr.has('extensionHasConfiguration')) + } + }); + } + + async run(accessor: ServicesAccessor, id: string) { + await accessor.get(IPreferencesService).openSettings(false, `@ext:${id}`); + } +}); + const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); class ExtensionsContributions implements IWorkbenchContribution { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 74df619bc6..4f6c209a50 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -39,7 +39,7 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { PagedModel } from 'vs/base/common/paging'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; +import { MenuRegistry, MenuId, IMenuService } from 'vs/platform/actions/common/actions'; import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IOpenerService } from 'vs/platform/opener/common/opener'; @@ -55,10 +55,8 @@ import { coalesce } from 'vs/base/common/arrays'; import { IWorkbenchThemeService, COLOR_THEME_SETTING, ICON_THEME_SETTING, IFileIconTheme, IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { ILabelService } from 'vs/platform/label/common/label'; import { prefersExecuteOnUI, prefersExecuteOnWorkspace } from 'vs/workbench/services/extensions/common/extensionsUtil'; -import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IFileDialogService, IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; @@ -681,6 +679,14 @@ export class DropDownMenuActionViewItem extends ExtensionActionViewItem { } } +export function getContextMenuActions(menuService: IMenuService, contextKeyService: IContextKeyService): ExtensionAction[][] { + const groups: ExtensionAction[][] = []; + const menu = menuService.createMenu(MenuId.ExtensionContext, contextKeyService); + menu.getActions({ shouldForwardArgs: true }).forEach(([, actions]) => groups.push(actions.map(action => new MenuItemExtensionAction(action)))); + menu.dispose(); + return groups; +} + export class ManageExtensionAction extends ExtensionDropDownAction { static readonly ID = 'extensions.manage'; @@ -690,7 +696,9 @@ export class ManageExtensionAction extends ExtensionDropDownAction { constructor( @IInstantiationService instantiationService: IInstantiationService, @IExtensionService private readonly extensionService: IExtensionService, - @IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService + @IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService, + @IMenuService private readonly menuService: IMenuService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { super(ManageExtensionAction.ID, '', '', true, true, instantiationService); @@ -727,11 +735,10 @@ export class ManageExtensionAction extends ExtensionDropDownAction { groups.push([this.instantiationService.createInstance(UninstallAction)]); groups.push([this.instantiationService.createInstance(InstallAnotherVersionAction)]); - const extensionActions: ExtensionAction[] = [this.instantiationService.createInstance(CopyExtensionInfoAction), this.instantiationService.createInstance(CopyExtensionIdAction)]; - if (this.extension && this.extension.local && this.extension.local.manifest.contributes && this.extension.local.manifest.contributes.configuration) { - extensionActions.push(this.instantiationService.createInstance(ExtensionSettingsAction)); - } - groups.push(extensionActions); + const contextKeyService = this.contextKeyService.createScoped(); + contextKeyService.createKey('extensionStatus', 'installed'); + contextKeyService.createKey('extensionHasConfiguration', !!this.extension && !!this.extension.local && !!this.extension.local.manifest.contributes && !!this.extension.local.manifest.contributes.configuration); + getContextMenuActions(this.menuService, contextKeyService).forEach(actions => groups.push(actions)); groups.forEach(group => group.forEach(extensionAction => extensionAction.extension = this.extension)); @@ -757,6 +764,21 @@ export class ManageExtensionAction extends ExtensionDropDownAction { } } +export class MenuItemExtensionAction extends ExtensionAction { + + constructor(private readonly action: IAction) { + super(action.id, action.label); + } + + update() { } + + async run(): Promise { + if (this.extension) { + return this.action.run(this.extension.identifier.id); + } + } +} + export class InstallAnotherVersionAction extends ExtensionAction { static readonly ID = 'workbench.extensions.action.install.anotherVersion'; @@ -809,89 +831,6 @@ export class InstallAnotherVersionAction extends ExtensionAction { } } -export class CopyExtensionInfoAction extends ExtensionAction { - - static readonly ID = 'workbench.extensions.action.copyExtension'; - static readonly LABEL = localize('workbench.extensions.action.copyExtension', "Copy"); - - constructor( - @IClipboardService private readonly clipboardService: IClipboardService - ) { - super(CopyExtensionInfoAction.ID, CopyExtensionInfoAction.LABEL); - this.update(); - } - - update(): void { - this.enabled = !!this.extension; - } - - async run(): Promise { - if (!this.extension) { - return; - } - - const name = localize('extensionInfoName', 'Name: {0}', this.extension.displayName); - const id = localize('extensionInfoId', 'Id: {0}', this.extension.identifier.id); - const description = localize('extensionInfoDescription', 'Description: {0}', this.extension.description); - const verision = localize('extensionInfoVersion', 'Version: {0}', this.extension.version); - const publisher = localize('extensionInfoPublisher', 'Publisher: {0}', this.extension.publisherDisplayName); - const link = this.extension.url ? localize('extensionInfoVSMarketplaceLink', 'VS Marketplace Link: {0}', this.extension.url.toString()) : null; - - const clipboardStr = `${name}\n${id}\n${description}\n${verision}\n${publisher}${link ? '\n' + link : ''}`; - - return this.clipboardService.writeText(clipboardStr); - } -} - -export class CopyExtensionIdAction extends ExtensionAction { - - static readonly ID = 'workbench.extensions.action.copyExtensionId'; - static readonly LABEL = localize('workbench.extensions.action.copyExtensionId', "Copy Extension Id"); - - constructor( - @IClipboardService private readonly clipboardService: IClipboardService - ) { - super(CopyExtensionIdAction.ID, CopyExtensionIdAction.LABEL); - this.update(); - } - - update(): void { - this.enabled = !!this.extension; - } - - async run(): Promise { - if (!this.extension) { - return; - } - return this.clipboardService.writeText(this.extension.identifier.id); - } -} - -export class ExtensionSettingsAction extends ExtensionAction { - - static readonly ID = 'extensions.extensionSettings'; - static readonly LABEL = localize('extensionSettingsAction', "Configure Extension Settings"); - - constructor( - @IPreferencesService private readonly preferencesService: IPreferencesService - ) { - super(ExtensionSettingsAction.ID, ExtensionSettingsAction.LABEL); - this.update(); - } - - update(): void { - this.enabled = !!this.extension; - } - - async run(): Promise { - if (!this.extension) { - return; - } - this.preferencesService.openSettings(false, `@ext:${this.extension.identifier.id}`); - return Promise.resolve(); - } -} - export class EnableForWorkspaceAction extends ExtensionAction { static readonly ID = 'extensions.enableForWorkspace'; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index 001ac5e266..21d9f0cf9f 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -57,6 +57,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { RemoteNameContext } from 'vs/workbench/browser/contextkeys'; import { ILabelService } from 'vs/platform/label/common/label'; import { MementoObject } from 'vs/workbench/common/memento'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; const NonEmptyWorkspaceContext = new RawContextKey('nonEmptyWorkspace', false); const DefaultViewsContext = new RawContextKey('defaultExtensionViews', true); @@ -130,7 +131,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio return { id, name: viewIdNameMappings[id], - ctorDescriptor: { ctor: ExtensionsListView }, + ctorDescriptor: new SyncDescriptor(ExtensionsListView), when: ContextKeyExpr.and(ContextKeyExpr.has('searchMarketplaceExtensions')), weight: 100 }; @@ -143,7 +144,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio return { id, name: viewIdNameMappings[id], - ctorDescriptor: { ctor: EnabledExtensionsView }, + ctorDescriptor: new SyncDescriptor(EnabledExtensionsView), when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions'), RemoteNameContext.isEqualTo('')), weight: 40, canToggleVisibility: true, @@ -158,7 +159,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio return { id, name: viewIdNameMappings[id], - ctorDescriptor: { ctor: DisabledExtensionsView }, + ctorDescriptor: new SyncDescriptor(DisabledExtensionsView), when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions'), RemoteNameContext.isEqualTo('')), weight: 10, canToggleVisibility: true, @@ -175,7 +176,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio return { id, name: viewIdNameMappings[id], - ctorDescriptor: { ctor: ExtensionsListView }, + ctorDescriptor: new SyncDescriptor(ExtensionsListView), when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.not('hasInstalledExtensions')), weight: 60, order: 1 @@ -197,19 +198,19 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio return [{ id: `extensions.${server.authority}.installed`, get name() { return getInstalledViewName(); }, - ctorDescriptor: { ctor: ServerExtensionsView, arguments: [server, EventOf.map(onDidChangeServerLabel, () => getInstalledViewName())] }, + ctorDescriptor: new SyncDescriptor(ServerExtensionsView, [server, EventOf.map(onDidChangeServerLabel, () => getInstalledViewName())]), when: ContextKeyExpr.and(ContextKeyExpr.has('searchInstalledExtensions')), weight: 100 }, { id: `extensions.${server.authority}.outdated`, get name() { return getOutdatedViewName(); }, - ctorDescriptor: { ctor: ServerExtensionsView, arguments: [server, EventOf.map(onDidChangeServerLabel, () => getOutdatedViewName())] }, + ctorDescriptor: new SyncDescriptor(ServerExtensionsView, [server, EventOf.map(onDidChangeServerLabel, () => getOutdatedViewName())]), when: ContextKeyExpr.and(ContextKeyExpr.has('searchOutdatedExtensions')), weight: 100 }, { id: `extensions.${server.authority}.default`, get name() { return getInstalledViewName(); }, - ctorDescriptor: { ctor: ServerExtensionsView, arguments: [server, EventOf.map(onDidChangeServerLabel, () => getInstalledViewName())] }, + ctorDescriptor: new SyncDescriptor(ServerExtensionsView, [server, EventOf.map(onDidChangeServerLabel, () => getInstalledViewName())]), when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions'), RemoteNameContext.notEqualsTo('')), weight: 40, order: 1 @@ -224,7 +225,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio return { id, name: viewIdNameMappings[id], - ctorDescriptor: { ctor: DefaultRecommendedExtensionsView }, + ctorDescriptor: new SyncDescriptor(DefaultRecommendedExtensionsView), when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('defaultRecommendedExtensions')), weight: 40, order: 2, @@ -239,7 +240,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio return { id, name: viewIdNameMappings[id], - ctorDescriptor: { ctor: RecommendedExtensionsView }, + ctorDescriptor: new SyncDescriptor(RecommendedExtensionsView), when: ContextKeyExpr.has('recommendedExtensions'), weight: 50, order: 2 @@ -253,7 +254,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio return { id, name: viewIdNameMappings[id], - ctorDescriptor: { ctor: WorkspaceRecommendedExtensionsView }, + ctorDescriptor: new SyncDescriptor(WorkspaceRecommendedExtensionsView), when: ContextKeyExpr.and(ContextKeyExpr.has('recommendedExtensions'), ContextKeyExpr.has('nonEmptyWorkspace')), weight: 50, order: 1 @@ -265,7 +266,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio return { id, name: viewIdNameMappings[id], - ctorDescriptor: { ctor: EnabledExtensionsView }, + ctorDescriptor: new SyncDescriptor(EnabledExtensionsView), when: ContextKeyExpr.and(ContextKeyExpr.has('searchEnabledExtensions')), weight: 40, order: 1 @@ -277,7 +278,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio return { id, name: viewIdNameMappings[id], - ctorDescriptor: { ctor: DisabledExtensionsView }, + ctorDescriptor: new SyncDescriptor(DisabledExtensionsView), when: ContextKeyExpr.and(ContextKeyExpr.has('searchDisabledExtensions')), weight: 10, order: 3, @@ -290,7 +291,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio return { id, name: viewIdNameMappings[id], - ctorDescriptor: { ctor: BuiltInExtensionsView }, + ctorDescriptor: new SyncDescriptor(BuiltInExtensionsView), when: ContextKeyExpr.has('searchBuiltInExtensions'), weight: 100 }; @@ -301,7 +302,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio return { id, name: viewIdNameMappings[id], - ctorDescriptor: { ctor: BuiltInThemesExtensionsView }, + ctorDescriptor: new SyncDescriptor(BuiltInThemesExtensionsView), when: ContextKeyExpr.has('searchBuiltInExtensions'), weight: 100 }; @@ -312,7 +313,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio return { id, name: viewIdNameMappings[id], - ctorDescriptor: { ctor: BuiltInBasicsExtensionsView }, + ctorDescriptor: new SyncDescriptor(BuiltInBasicsExtensionsView), when: ContextKeyExpr.has('searchBuiltInExtensions'), weight: 100 }; @@ -587,9 +588,8 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE if (this.configurationService.getValue(CloseExtensionDetailsOnViewChangeKey)) { const promises = this.editorGroupService.groups.map(group => { const editors = group.editors.filter(input => input instanceof ExtensionsInput); - const promises = editors.map(editor => group.closeEditor(editor)); - return Promise.all(promises); + return group.closeEditors(editors); }); Promise.all(promises); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index a6cc2f38af..5c1c904389 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -28,7 +28,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; -import { InstallWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, ManageExtensionAction, InstallLocalExtensionsInRemoteAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { InstallWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, ManageExtensionAction, InstallLocalExtensionsInRemoteAction, getContextMenuActions } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { WorkbenchPagedList } from 'vs/platform/list/browser/listService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; @@ -48,6 +48,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { SeverityIcon } from 'vs/platform/severityIcon/common/severityIcon'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; +import { IMenuService } from 'vs/platform/actions/common/actions'; // Extensions that are automatically classified as Programming Language extensions, but should be Feature extensions const FORCE_FEATURE_EXTENSIONS = ['vscode.git', 'vscode.search-result']; @@ -106,9 +107,10 @@ export class ExtensionsListView extends ViewPane { @IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService, @IExtensionManagementServerService protected readonly extensionManagementServerService: IExtensionManagementServerService, @IProductService protected readonly productService: IProductService, - @IContextKeyService contextKeyService: IContextKeyService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IMenuService private readonly menuService: IMenuService, ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: options.title, showActionsAlways: true }, keybindingService, contextMenuService, configurationService, contextKeyService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: options.title, showActionsAlways: true }, keybindingService, contextMenuService, configurationService, contextKeyService, instantiationService); this.server = options.server; } @@ -228,16 +230,27 @@ export class ExtensionsListView extends ViewPane { const fileIconThemes = await this.workbenchThemeService.getFileIconThemes(); const manageExtensionAction = this.instantiationService.createInstance(ManageExtensionAction); manageExtensionAction.extension = e.element; - const groups = manageExtensionAction.getActionGroups(runningExtensions, colorThemes, fileIconThemes); - let actions: IAction[] = []; - for (const menuActions of groups) { - actions = [...actions, ...menuActions, new Separator()]; - } if (manageExtensionAction.enabled) { + const groups = manageExtensionAction.getActionGroups(runningExtensions, colorThemes, fileIconThemes); + let actions: IAction[] = []; + for (const menuActions of groups) { + actions = [...actions, ...menuActions, new Separator()]; + } this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, getActions: () => actions.slice(0, actions.length - 1) }); + } else if (e.element) { + const groups = getContextMenuActions(this.menuService, this.contextKeyService.createScoped()); + groups.forEach(group => group.forEach(extensionAction => extensionAction.extension = e.element!)); + let actions: IAction[] = []; + for (const menuActions of groups) { + actions = [...actions, ...menuActions, new Separator()]; + } + this.contextMenuService.showContextMenu({ + getAnchor: () => e.anchor, + getActions: () => actions + }); } } } @@ -949,10 +962,11 @@ export class ServerExtensionsView extends ExtensionsListView { @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService, @IProductService productService: IProductService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @IMenuService menuService: IMenuService, ) { options.server = server; - super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, editorService, tipsService, telemetryService, configurationService, contextService, experimentService, workbenchThemeService, extensionManagementServerService, productService, contextKeyService); + super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, editorService, tipsService, telemetryService, configurationService, contextService, experimentService, workbenchThemeService, extensionManagementServerService, productService, contextKeyService, menuService); this._register(onDidChangeTitle(title => this.updateTitle(title))); } diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css index 3dbadc23a9..55ab74f10d 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css @@ -6,6 +6,8 @@ .extension-editor { height: 100%; overflow: hidden; + display: flex; + flex-direction: column; } .extension-editor .clickable { @@ -188,7 +190,7 @@ } .extension-editor > .body { - height: calc(100% - 168px); + flex: 1; overflow: hidden; } diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index 60ddf618ff..e81bacdbc6 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -14,7 +14,6 @@ import { areSameExtensions } from 'vs/platform/extensionManagement/common/extens import { IExtensionManifest, ExtensionType } from 'vs/platform/extensions/common/extensions'; import { URI } from 'vs/base/common/uri'; import { IViewPaneContainer } from 'vs/workbench/common/viewPaneContainer'; -import { IAction } from 'vs/base/common/actions'; export const VIEWLET_ID = 'workbench.view.extensions'; @@ -145,12 +144,3 @@ export class ExtensionContainers extends Disposable { } } } - -export interface IExtensionMenuAction extends IAction { - run(context: IExtensionMenuActionContext): Promise; -} - -export interface IExtensionMenuActionContext { - id: string; - packageJSON: IExtensionManifest; -} diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts index 3390c8b3b6..5b9f8aa5b2 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts @@ -27,7 +27,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { TestContextService, TestSharedProcessService } from 'vs/workbench/test/workbenchTestServices'; +import { TestContextService, TestSharedProcessService, TestMenuService } from 'vs/workbench/test/workbenchTestServices'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { URLService } from 'vs/platform/url/node/urlService'; @@ -44,6 +44,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { ILabelService } from 'vs/platform/label/common/label'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; +import { IMenuService } from 'vs/platform/actions/common/actions'; suite('ExtensionsListView Tests', () => { @@ -92,7 +93,8 @@ suite('ExtensionsListView Tests', () => { instantiationService.stub(IExtensionManagementService, 'onUninstallExtension', uninstallEvent.event); instantiationService.stub(IExtensionManagementService, 'onDidUninstallExtension', didUninstallEvent.event); instantiationService.stub(IRemoteAgentService, RemoteAgentService); - instantiationService.stub(IContextKeyService, MockContextKeyService); + instantiationService.stub(IContextKeyService, new MockContextKeyService()); + instantiationService.stub(IMenuService, new TestMenuService()); instantiationService.stub(IExtensionManagementServerService, new class extends ExtensionManagementServerService { private _localExtensionManagementServer: IExtensionManagementServer = { extensionManagementService: instantiationService.get(IExtensionManagementService), label: 'local', authority: 'vscode-local' }; diff --git a/src/vs/workbench/contrib/feedback/browser/feedback.ts b/src/vs/workbench/contrib/feedback/browser/feedback.ts index a5548221d1..006d772c1d 100644 --- a/src/vs/workbench/contrib/feedback/browser/feedback.ts +++ b/src/vs/workbench/contrib/feedback/browser/feedback.ts @@ -277,12 +277,12 @@ export class FeedbackDropdown extends Dropdown { disposables.add(attachStylerCallback(this.themeService, { widgetShadow, editorWidgetBackground, editorWidgetForeground, inputBackground, inputForeground, inputBorder, editorBackground, contrastBorder }, colors => { if (this.feedbackForm) { this.feedbackForm.style.backgroundColor = colors.editorWidgetBackground ? colors.editorWidgetBackground.toString() : ''; - this.feedbackForm.style.color = colors.editorWidgetForeground ? colors.editorWidgetForeground.toString() : null; + this.feedbackForm.style.color = colors.editorWidgetForeground ? colors.editorWidgetForeground.toString() : ''; this.feedbackForm.style.boxShadow = colors.widgetShadow ? `0 0 8px ${colors.widgetShadow}` : ''; } if (this.feedbackDescriptionInput) { this.feedbackDescriptionInput.style.backgroundColor = colors.inputBackground ? colors.inputBackground.toString() : ''; - this.feedbackDescriptionInput.style.color = colors.inputForeground ? colors.inputForeground.toString() : null; + this.feedbackDescriptionInput.style.color = colors.inputForeground ? colors.inputForeground.toString() : ''; this.feedbackDescriptionInput.style.border = `1px solid ${colors.inputBorder || 'transparent'}`; } diff --git a/src/vs/workbench/contrib/feedback/browser/feedbackStatusbarItem.ts b/src/vs/workbench/contrib/feedback/browser/feedbackStatusbarItem.ts index 213a39485d..3d1ce3bdb8 100644 --- a/src/vs/workbench/contrib/feedback/browser/feedbackStatusbarItem.ts +++ b/src/vs/workbench/contrib/feedback/browser/feedbackStatusbarItem.ts @@ -94,7 +94,8 @@ export class FeedbackStatusbarConribution extends Disposable implements IWorkben private getStatusEntry(showBeak?: boolean): IStatusbarEntry { return { - text: '$(smiley)', + text: '$(feedback)', + tooltip: localize('status.feedback', "Tweet Feedback"), command: '_feedback.open', showBeak }; diff --git a/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts b/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts index 0d6568eb98..468d2082cd 100644 --- a/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts +++ b/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts @@ -7,7 +7,7 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { URI } from 'vs/base/common/uri'; import { IEditorViewState } from 'vs/editor/common/editorCommon'; import { toResource, SideBySideEditorInput, IWorkbenchEditorConfiguration, SideBySideEditor as SideBySideEditorChoice } from 'vs/workbench/common/editor'; -import { ITextFileService, TextFileModelChangeEvent, ModelState } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileService, ModelState } from 'vs/workbench/services/textfile/common/textfiles'; import { FileOperationEvent, FileOperation, IFileService, FileChangeType, FileChangesEvent, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; @@ -21,7 +21,7 @@ import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { timeout } from 'vs/base/common/async'; +import { timeout, RunOnceWorker } from 'vs/base/common/async'; import { withNullAsUndefined } from 'vs/base/common/types'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { isEqualOrParent, joinPath } from 'vs/base/common/resources'; @@ -33,8 +33,6 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut private readonly activeOutOfWorkspaceWatchers = new ResourceMap(); - private closeOnFileDelete: boolean = false; - constructor( @IEditorService private readonly editorService: IEditorService, @ITextFileService private readonly textFileService: ITextFileService, @@ -45,7 +43,7 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IHostService private readonly hostService: IHostService, - @ICodeEditorService private readonly codeEditorService: ICodeEditorService, + @ICodeEditorService private readonly codeEditorService: ICodeEditorService ) { super(); @@ -62,8 +60,10 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut // Update editors from disk changes this._register(this.fileService.onFileChanges(e => this.onFileChanges(e))); - // Open editors from dirty text file models - this._register(this.textFileService.models.onModelsDirty(e => this.onTextFilesDirty(e))); + // Ensure dirty text file and untitled models are always opened as editors + this._register(this.textFileService.files.onDidChangeDirty(m => this.ensureDirtyFilesAreOpenedWorker.work(m.resource))); + this._register(this.textFileService.files.onDidSaveError(m => this.ensureDirtyFilesAreOpenedWorker.work(m.resource))); + this._register(this.textFileService.untitled.onDidChangeDirty(r => this.ensureDirtyFilesAreOpenedWorker.work(r))); // Out of workspace file watchers this._register(this.editorService.onDidVisibleEditorsChange(() => this.onDidVisibleEditorsChange())); @@ -115,7 +115,7 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut } let encoding: string | undefined = undefined; - const model = this.textFileService.models.get(resource); + const model = this.textFileService.files.get(resource); if (model) { encoding = model.getEncoding(); } @@ -177,7 +177,17 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut //#endregion - //#region File Changes: Close editors of deleted files + //#region File Changes: Close editors of deleted files unless configured otherwise + + private closeOnFileDelete: boolean = false; + + private onConfigurationUpdated(configuration: IWorkbenchEditorConfiguration): void { + if (typeof configuration.workbench?.editor?.closeOnFileDelete === 'boolean') { + this.closeOnFileDelete = configuration.workbench.editor.closeOnFileDelete; + } else { + this.closeOnFileDelete = false; // default + } + } private onFileChanges(e: FileChangesEvent): void { if (e.gotDeleted()) { @@ -268,29 +278,38 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut //#endregion - //#region Text File Dirty: Ensure every dirty text file is opened in an editor + //#region Text File: Ensure every dirty text and untitled file is opened in an editor - private onTextFilesDirty(events: ReadonlyArray): void { + private readonly ensureDirtyFilesAreOpenedWorker = this._register(new RunOnceWorker(units => this.ensureDirtyFilesAreOpened(units), 250)); - // If files become dirty but are not opened, we open it in the background unless there are pending to be saved - this.doOpenDirtyResourcesInBackground(distinct(events.filter(({ resource }) => { + private ensureDirtyFilesAreOpened(resources: URI[]): void { + this.doEnsureDirtyFilesAreOpened(distinct(resources.filter(resource => { + if (!this.textFileService.isDirty(resource)) { + return false; // resource must be dirty + } - // Only dirty models that are not PENDING_SAVE - const model = this.textFileService.models.get(resource); - const shouldOpen = model?.isDirty() && !model.hasState(ModelState.PENDING_SAVE); + const model = this.textFileService.files.get(resource); + if (model?.hasState(ModelState.PENDING_SAVE)) { + return false; // resource must not be pending to save + } - // Only if not open already - return shouldOpen && !this.editorService.isOpen({ resource }); - }).map(event => event.resource), resource => resource.toString())); + if (this.editorService.isOpen({ resource })) { + return false; // model must not be opened already + } + + return true; + }), resource => resource.toString())); } - private doOpenDirtyResourcesInBackground(resources: URI[]): void { - this.editorService.openEditors(resources.map(resource => { - return { - resource, - options: { inactive: true, pinned: true, preserveFocus: true } - }; - })); + private doEnsureDirtyFilesAreOpened(resources: URI[]): void { + if (!resources.length) { + return; + } + + this.editorService.openEditors(resources.map(resource => ({ + resource, + options: { inactive: true, pinned: true, preserveFocus: true } + }))); } //#endregion @@ -348,7 +367,7 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut return undefined; } - const model = this.textFileService.models.get(resource); + const model = this.textFileService.files.get(resource); if (!model) { return undefined; } @@ -366,18 +385,6 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut //#endregion - //#region Configuration Change - - private onConfigurationUpdated(configuration: IWorkbenchEditorConfiguration): void { - if (typeof configuration.workbench?.editor?.closeOnFileDelete === 'boolean') { - this.closeOnFileDelete = configuration.workbench.editor.closeOnFileDelete; - } else { - this.closeOnFileDelete = false; // default - } - } - - //#endregion - dispose(): void { super.dispose(); diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts index 8f93c6f0d6..46a9c649fc 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts @@ -71,8 +71,8 @@ export class TextFileSaveErrorHandler extends Disposable implements ISaveErrorHa } private registerListeners(): void { - this._register(this.textFileService.models.onModelSaved(e => this.onFileSavedOrReverted(e.resource))); - this._register(this.textFileService.models.onModelReverted(e => this.onFileSavedOrReverted(e.resource))); + this._register(this.textFileService.files.onDidSave(e => this.onFileSavedOrReverted(e.model.resource))); + this._register(this.textFileService.files.onDidRevert(m => this.onFileSavedOrReverted(m.resource))); this._register(this.editorService.onDidActiveEditorChange(() => this.onActiveEditorChanged())); } @@ -180,7 +180,7 @@ export class TextFileSaveErrorHandler extends Disposable implements ISaveErrorHa // Show message and keep function to hide in case the file gets saved/reverted const actions: INotificationActions = { primary: primaryActions, secondary: secondaryActions }; const handle = this.notificationService.notify({ severity: Severity.Error, message, actions }); - Event.once(handle.onDidClose)(() => { dispose(primaryActions), dispose(secondaryActions); }); + Event.once(handle.onDidClose)(() => { dispose(primaryActions); dispose(secondaryActions); }); this.messages.set(model.resource, handle); } diff --git a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts index 9e4cd8d01f..e27b2aadb9 100644 --- a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts +++ b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts @@ -33,6 +33,7 @@ import { ViewPane, ViewPaneContainer } from 'vs/workbench/browser/parts/views/vi import { KeyChord, KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { Registry } from 'vs/platform/registry/common/platform'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; export class ExplorerViewletViewsContribution extends Disposable implements IWorkbenchContribution { @@ -103,7 +104,7 @@ export class ExplorerViewletViewsContribution extends Disposable implements IWor return { id: OpenEditorsView.ID, name: OpenEditorsView.NAME, - ctorDescriptor: { ctor: OpenEditorsView }, + ctorDescriptor: new SyncDescriptor(OpenEditorsView), order: 0, when: OpenEditorsVisibleContext, canToggleVisibility: true, @@ -118,7 +119,7 @@ export class ExplorerViewletViewsContribution extends Disposable implements IWor return { id: EmptyView.ID, name: EmptyView.NAME, - ctorDescriptor: { ctor: EmptyView }, + ctorDescriptor: new SyncDescriptor(EmptyView), order: 1, canToggleVisibility: true, }; @@ -128,7 +129,7 @@ export class ExplorerViewletViewsContribution extends Disposable implements IWor return { id: ExplorerView.ID, name: localize('folders', "Folders"), - ctorDescriptor: { ctor: ExplorerView }, + ctorDescriptor: new SyncDescriptor(ExplorerView), order: 1, canToggleVisibility: false }; @@ -247,7 +248,7 @@ export class ExplorerViewPaneContainer extends ViewPaneContainer { export const VIEW_CONTAINER: ViewContainer = Registry.as(Extensions.ViewContainersRegistry).registerViewContainer({ id: VIEWLET_ID, name: localize('explore', "Explorer"), - ctorDescriptor: { ctor: ExplorerViewPaneContainer }, + ctorDescriptor: new SyncDescriptor(ExplorerViewPaneContainer), icon: 'codicon-files', order: 10 // {{SQL CARBON EDIT}} }, ViewContainerLocation.Sidebar); diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index 47e5afc346..2e3989e997 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -33,14 +33,14 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Schemas } from 'vs/base/common/network'; -import { IDialogService, IConfirmationResult, getConfirmMessage, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IDialogService, IConfirmationResult, getFileNamesMessage, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { Constants } from 'vs/base/common/uint'; import { CLOSE_EDITORS_AND_GROUP_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; import { coalesce } from 'vs/base/common/arrays'; import { ExplorerItem, NewExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; -import { onUnexpectedError, getErrorMessage } from 'vs/base/common/errors'; +import { getErrorMessage } from 'vs/base/common/errors'; import { asDomUri, triggerDownload } from 'vs/base/browser/dom'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; @@ -164,7 +164,7 @@ export class GlobalNewUntitledFileAction extends Action { } } -async function deleteFiles(textFileService: ITextFileService, dialogService: IDialogService, configurationService: IConfigurationService, elements: ExplorerItem[], useTrash: boolean, skipConfirm = false): Promise { +async function deleteFiles(workingCopyService: IWorkingCopyService, textFileService: ITextFileService, dialogService: IDialogService, configurationService: IConfigurationService, elements: ExplorerItem[], useTrash: boolean, skipConfirm = false): Promise { let primaryButton: string; if (useTrash) { primaryButton = isWindows ? nls.localize('deleteButtonLabelRecycleBin', "&&Move to Recycle Bin") : nls.localize({ key: 'deleteButtonLabelTrash', comment: ['&& denotes a mnemonic'] }, "&&Move to Trash"); @@ -176,19 +176,19 @@ async function deleteFiles(textFileService: ITextFileService, dialogService: IDi // Handle dirty let confirmed = true; - const dirty = textFileService.getDirty().filter(d => distinctElements.some(e => resources.isEqualOrParent(d, e.resource))); - if (dirty.length) { + const dirtyWorkingCopies = workingCopyService.dirtyWorkingCopies.filter(workingCopy => distinctElements.some(e => resources.isEqualOrParent(workingCopy.resource, e.resource))); + if (dirtyWorkingCopies.length) { let message: string; if (distinctElements.length > 1) { message = nls.localize('dirtyMessageFilesDelete', "You are deleting files with unsaved changes. Do you want to continue?"); } else if (distinctElements[0].isDirectory) { - if (dirty.length === 1) { - message = nls.localize('dirtyMessageFolderOneDelete', "You are deleting a folder with unsaved changes in 1 file. Do you want to continue?"); + if (dirtyWorkingCopies.length === 1) { + message = nls.localize('dirtyMessageFolderOneDelete', "You are deleting a folder {0} with unsaved changes in 1 file. Do you want to continue?", distinctElements[0].name); } else { - message = nls.localize('dirtyMessageFolderDelete', "You are deleting a folder with unsaved changes in {0} files. Do you want to continue?", dirty.length); + message = nls.localize('dirtyMessageFolderDelete', "You are deleting a folder {0} with unsaved changes in {1} files. Do you want to continue?", distinctElements[0].name, dirtyWorkingCopies.length); } } else { - message = nls.localize('dirtyMessageFileDelete', "You are deleting a file with unsaved changes. Do you want to continue?"); + message = nls.localize('dirtyMessageFileDelete', "You are deleting {0} with unsaved changes. Do you want to continue?", distinctElements[0].name); } const response = await dialogService.confirm({ @@ -202,7 +202,7 @@ async function deleteFiles(textFileService: ITextFileService, dialogService: IDi confirmed = false; } else { skipConfirm = true; - await textFileService.revertAll(dirty); + await Promise.all(dirtyWorkingCopies.map(dirty => dirty.revert())); } } @@ -211,20 +211,26 @@ async function deleteFiles(textFileService: ITextFileService, dialogService: IDi return; } - let confirmDeletePromise: Promise; + let confirmation: IConfirmationResult; // Check if we need to ask for confirmation at all if (skipConfirm || (useTrash && configurationService.getValue(CONFIRM_DELETE_SETTING_KEY) === false)) { - confirmDeletePromise = Promise.resolve({ confirmed: true }); + confirmation = { confirmed: true }; } // Confirm for moving to trash else if (useTrash) { - const message = getMoveToTrashMessage(distinctElements); + let { message, detail } = getMoveToTrashMessage(distinctElements); + detail += detail ? '\n' : ''; + if (isWindows) { + detail += distinctElements.length > 1 ? nls.localize('undoBinFiles', "You can restore these files from the Recycle Bin.") : nls.localize('undoBin', "You can restore this file from the Recycle Bin."); + } else { + detail += distinctElements.length > 1 ? nls.localize('undoTrashFiles', "You can restore these files from the Trash.") : nls.localize('undoTrash', "You can restore this file from the Trash."); + } - confirmDeletePromise = dialogService.confirm({ + confirmation = await dialogService.confirm({ message, - detail: isWindows ? nls.localize('undoBin', "You can restore from the Recycle Bin.") : nls.localize('undoTrash', "You can restore from the Trash."), + detail, primaryButton, checkbox: { label: nls.localize('doNotAskAgain', "Do not ask me again") @@ -235,110 +241,123 @@ async function deleteFiles(textFileService: ITextFileService, dialogService: IDi // Confirm for deleting permanently else { - const message = getDeleteMessage(distinctElements); - confirmDeletePromise = dialogService.confirm({ + let { message, detail } = getDeleteMessage(distinctElements); + detail += detail ? '\n' : ''; + detail += nls.localize('irreversible', "This action is irreversible!"); + confirmation = await dialogService.confirm({ message, - detail: nls.localize('irreversible', "This action is irreversible!"), + detail, primaryButton, type: 'warning' }); } - return confirmDeletePromise.then(confirmation => { - // Check for confirmation checkbox - let updateConfirmSettingsPromise: Promise = Promise.resolve(undefined); - if (confirmation.confirmed && confirmation.checkboxChecked === true) { - updateConfirmSettingsPromise = configurationService.updateValue(CONFIRM_DELETE_SETTING_KEY, false, ConfigurationTarget.USER); + + // Check for confirmation checkbox + if (confirmation.confirmed && confirmation.checkboxChecked === true) { + await configurationService.updateValue(CONFIRM_DELETE_SETTING_KEY, false, ConfigurationTarget.USER); + } + + + // Check for confirmation + if (!confirmation.confirmed) { + return; + } + + // Call function + try { + await Promise.all(distinctElements.map(e => textFileService.delete(e.resource, { useTrash: useTrash, recursive: true }))); + } catch (error) { + + // Handle error to delete file(s) from a modal confirmation dialog + let errorMessage: string; + let detailMessage: string | undefined; + let primaryButton: string; + if (useTrash) { + errorMessage = isWindows ? nls.localize('binFailed', "Failed to delete using the Recycle Bin. Do you want to permanently delete instead?") : nls.localize('trashFailed', "Failed to delete using the Trash. Do you want to permanently delete instead?"); + detailMessage = nls.localize('irreversible', "This action is irreversible!"); + primaryButton = nls.localize({ key: 'deletePermanentlyButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Delete Permanently"); + } else { + errorMessage = toErrorMessage(error, false); + primaryButton = nls.localize({ key: 'retryButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Retry"); } - return updateConfirmSettingsPromise.then(() => { + const res = await dialogService.confirm({ + message: errorMessage, + detail: detailMessage, + type: 'warning', + primaryButton + }); - // Check for confirmation - if (!confirmation.confirmed) { - return Promise.resolve(undefined); + if (res.confirmed) { + if (useTrash) { + useTrash = false; // Delete Permanently } - // Call function - const servicePromise = Promise.all(distinctElements.map(e => textFileService.delete(e.resource, { useTrash: useTrash, recursive: true }))) - .then(undefined, (error: any) => { - // Handle error to delete file(s) from a modal confirmation dialog - let errorMessage: string; - let detailMessage: string | undefined; - let primaryButton: string; - if (useTrash) { - errorMessage = isWindows ? nls.localize('binFailed', "Failed to delete using the Recycle Bin. Do you want to permanently delete instead?") : nls.localize('trashFailed', "Failed to delete using the Trash. Do you want to permanently delete instead?"); - detailMessage = nls.localize('irreversible', "This action is irreversible!"); - primaryButton = nls.localize({ key: 'deletePermanentlyButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Delete Permanently"); - } else { - errorMessage = toErrorMessage(error, false); - primaryButton = nls.localize({ key: 'retryButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Retry"); - } + skipConfirm = true; - return dialogService.confirm({ - message: errorMessage, - detail: detailMessage, - type: 'warning', - primaryButton - }).then(res => { - - if (res.confirmed) { - if (useTrash) { - useTrash = false; // Delete Permanently - } - - skipConfirm = true; - - return deleteFiles(textFileService, dialogService, configurationService, elements, useTrash, skipConfirm); - } - - return Promise.resolve(); - }); - }); - - return servicePromise.then(undefined); - }); - }); + return deleteFiles(workingCopyService, textFileService, dialogService, configurationService, elements, useTrash, skipConfirm); + } + } } -function getMoveToTrashMessage(distinctElements: ExplorerItem[]): string { +function getMoveToTrashMessage(distinctElements: ExplorerItem[]): { message: string, detail: string } { if (containsBothDirectoryAndFile(distinctElements)) { - return getConfirmMessage(nls.localize('confirmMoveTrashMessageFilesAndDirectories', "Are you sure you want to delete the following {0} files/directories and their contents?", distinctElements.length), distinctElements.map(e => e.resource)); + return { + message: nls.localize('confirmMoveTrashMessageFilesAndDirectories', "Are you sure you want to delete the following {0} files/directories and their contents?", distinctElements.length), + detail: getFileNamesMessage(distinctElements.map(e => e.resource)) + }; } if (distinctElements.length > 1) { if (distinctElements[0].isDirectory) { - return getConfirmMessage(nls.localize('confirmMoveTrashMessageMultipleDirectories', "Are you sure you want to delete the following {0} directories and their contents?", distinctElements.length), distinctElements.map(e => e.resource)); + return { + message: nls.localize('confirmMoveTrashMessageMultipleDirectories', "Are you sure you want to delete the following {0} directories and their contents?", distinctElements.length), + detail: getFileNamesMessage(distinctElements.map(e => e.resource)) + }; } - return getConfirmMessage(nls.localize('confirmMoveTrashMessageMultiple', "Are you sure you want to delete the following {0} files?", distinctElements.length), distinctElements.map(e => e.resource)); + return { + message: nls.localize('confirmMoveTrashMessageMultiple', "Are you sure you want to delete the following {0} files?", distinctElements.length), + detail: getFileNamesMessage(distinctElements.map(e => e.resource)) + }; } if (distinctElements[0].isDirectory) { - return nls.localize('confirmMoveTrashMessageFolder', "Are you sure you want to delete '{0}' and its contents?", distinctElements[0].name); + return { message: nls.localize('confirmMoveTrashMessageFolder', "Are you sure you want to delete '{0}' and its contents?", distinctElements[0].name), detail: '' }; } - return nls.localize('confirmMoveTrashMessageFile', "Are you sure you want to delete '{0}'?", distinctElements[0].name); + return { message: nls.localize('confirmMoveTrashMessageFile', "Are you sure you want to delete '{0}'?", distinctElements[0].name), detail: '' }; } -function getDeleteMessage(distinctElements: ExplorerItem[]): string { +function getDeleteMessage(distinctElements: ExplorerItem[]): { message: string, detail: string } { if (containsBothDirectoryAndFile(distinctElements)) { - return getConfirmMessage(nls.localize('confirmDeleteMessageFilesAndDirectories', "Are you sure you want to permanently delete the following {0} files/directories and their contents?", distinctElements.length), distinctElements.map(e => e.resource)); + return { + message: nls.localize('confirmDeleteMessageFilesAndDirectories', "Are you sure you want to permanently delete the following {0} files/directories and their contents?", distinctElements.length), + detail: getFileNamesMessage(distinctElements.map(e => e.resource)) + }; } if (distinctElements.length > 1) { if (distinctElements[0].isDirectory) { - return getConfirmMessage(nls.localize('confirmDeleteMessageMultipleDirectories', "Are you sure you want to permanently delete the following {0} directories and their contents?", distinctElements.length), distinctElements.map(e => e.resource)); + return { + message: nls.localize('confirmDeleteMessageMultipleDirectories', "Are you sure you want to permanently delete the following {0} directories and their contents?", distinctElements.length), + detail: getFileNamesMessage(distinctElements.map(e => e.resource)) + }; } - return getConfirmMessage(nls.localize('confirmDeleteMessageMultiple', "Are you sure you want to permanently delete the following {0} files?", distinctElements.length), distinctElements.map(e => e.resource)); + return { + message: nls.localize('confirmDeleteMessageMultiple', "Are you sure you want to permanently delete the following {0} files?", distinctElements.length), + detail: getFileNamesMessage(distinctElements.map(e => e.resource)) + }; } if (distinctElements[0].isDirectory) { - return nls.localize('confirmDeleteMessageFolder', "Are you sure you want to permanently delete '{0}' and its contents?", distinctElements[0].name); + return { message: nls.localize('confirmDeleteMessageFolder', "Are you sure you want to permanently delete '{0}' and its contents?", distinctElements[0].name), detail: '' }; } - return nls.localize('confirmDeleteMessageFile', "Are you sure you want to permanently delete '{0}'?", distinctElements[0].name); + return { message: nls.localize('confirmDeleteMessageFile', "Are you sure you want to permanently delete '{0}'?", distinctElements[0].name), detail: '' }; } function containsBothDirectoryAndFile(distinctElements: ExplorerItem[]): boolean { @@ -643,7 +662,7 @@ export class ShowActiveFileInExplorer extends Action { super(id, label); } - run(): Promise { + async run(): Promise { const resource = toResource(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.MASTER }); if (resource) { this.commandService.executeCommand(REVEAL_IN_EXPLORER_COMMAND_ID, resource); @@ -651,7 +670,7 @@ export class ShowActiveFileInExplorer extends Action { this.notificationService.info(nls.localize('openFileToShow', "Open a file first to show it in the explorer")); } - return Promise.resolve(true); + return true; } } @@ -721,7 +740,7 @@ export class ShowOpenedFileInNewWindow extends Action { super(id, label); } - run(): Promise { + async run(): Promise { const fileResource = toResource(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.MASTER }); if (fileResource) { if (this.fileService.canHandleResource(fileResource)) { @@ -733,7 +752,7 @@ export class ShowOpenedFileInNewWindow extends Action { this.notificationService.info(nls.localize('openFileToShowInNewWindow.nofile', "Open a file first to open in new window")); } - return Promise.resolve(true); + return true; } } @@ -817,7 +836,7 @@ export class CompareWithClipboardAction extends Action { this.enabled = true; } - run(): Promise { + async run(): Promise { const resource = toResource(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.MASTER }); if (resource && (this.fileService.canHandleResource(resource) || resource.scheme === Schemas.untitled)) { if (!this.registrationDisposal) { @@ -834,7 +853,7 @@ export class CompareWithClipboardAction extends Action { }); } - return Promise.resolve(true); + return true; } dispose(): void { @@ -897,15 +916,17 @@ async function openExplorerAndCreate(accessor: ServicesAccessor, isFolder: boole folder.addChild(newStat); - const onSuccess = (value: string): Promise => { - const createPromise = isFolder ? fileService.createFolder(resources.joinPath(folder.resource, value)) : textFileService.create(resources.joinPath(folder.resource, value)); - return createPromise.then(created => { + const onSuccess = async (value: string): Promise => { + try { + const created = isFolder ? await fileService.createFolder(resources.joinPath(folder.resource, value)) : await textFileService.create(resources.joinPath(folder.resource, value)); refreshIfSeparator(value, explorerService); - return isFolder ? explorerService.select(created.resource, true) - : editorService.openEditor({ resource: created.resource, options: { pinned: true } }).then(() => undefined); - }, error => { + + isFolder ? + await explorerService.select(created.resource, true) : + await editorService.openEditor({ resource: created.resource, options: { pinned: true } }); + } catch (error) { onErrorWithRetry(notificationService, error, () => onSuccess(value)); - }); + } }; explorerService.setEditable(newStat, { @@ -937,6 +958,7 @@ CommandsRegistry.registerCommand({ export const renameHandler = (accessor: ServicesAccessor) => { const explorerService = accessor.get(IExplorerService); const textFileService = accessor.get(ITextFileService); + const notificationService = accessor.get(INotificationService); const stats = explorerService.getContext(false); const stat = stats.length > 0 ? stats[0] : undefined; @@ -946,12 +968,17 @@ export const renameHandler = (accessor: ServicesAccessor) => { explorerService.setEditable(stat, { validationMessage: value => validateFileName(stat, value), - onFinish: (value, success) => { + onFinish: async (value, success) => { if (success) { const parentResource = stat.parent!.resource; const targetResource = resources.joinPath(parentResource, value); if (stat.resource.toString() !== targetResource.toString()) { - textFileService.move(stat.resource, targetResource).then(() => refreshIfSeparator(value, explorerService), onUnexpectedError); + try { + await textFileService.move(stat.resource, targetResource); + refreshIfSeparator(value, explorerService); + } catch (e) { + notificationService.error(e); + } } } explorerService.setEditable(stat, null); @@ -963,7 +990,7 @@ export const moveFileToTrashHandler = async (accessor: ServicesAccessor) => { const explorerService = accessor.get(IExplorerService); const stats = explorerService.getContext(true).filter(s => !s.isRoot); if (stats.length) { - await deleteFiles(accessor.get(ITextFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), stats, true); + await deleteFiles(accessor.get(IWorkingCopyService), accessor.get(ITextFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), stats, true); } }; @@ -972,7 +999,7 @@ export const deleteFileHandler = async (accessor: ServicesAccessor) => { const stats = explorerService.getContext(true).filter(s => !s.isRoot); if (stats.length) { - await deleteFiles(accessor.get(ITextFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), stats, false); + await deleteFiles(accessor.get(IWorkingCopyService), accessor.get(ITextFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), stats, false); } }; @@ -1088,6 +1115,7 @@ export const pasteFileHandler = async (accessor: ServicesAccessor) => { if (pasteShouldMove) { // Cut is done. Make sure to clear cut state. explorerService.setToCopy([], false); + pasteShouldMove = false; } if (stats.length >= 1) { const stat = stats[0]; diff --git a/src/vs/workbench/contrib/files/browser/fileCommands.ts b/src/vs/workbench/contrib/files/browser/fileCommands.ts index fc52beeb8a..c018e480cf 100644 --- a/src/vs/workbench/contrib/files/browser/fileCommands.ts +++ b/src/vs/workbench/contrib/files/browser/fileCommands.ts @@ -200,7 +200,7 @@ CommandsRegistry.registerCommand({ CommandsRegistry.registerCommand({ id: COMPARE_SELECTED_COMMAND_ID, - handler: (accessor, resource: URI | object) => { + handler: async (accessor, resource: URI | object) => { const editorService = accessor.get(IEditorService); const explorerService = accessor.get(IExplorerService); const resources = getMultiSelectedResources(resource, accessor.get(IListService), editorService, explorerService); @@ -212,7 +212,7 @@ CommandsRegistry.registerCommand({ }); } - return Promise.resolve(true); + return true; } }); @@ -358,7 +358,7 @@ async function saveSelectedEditors(accessor: ServicesAccessor, options?: ISaveEd // Check that the resource of the model was not saved already if (resource && !editors.some(({ editor }) => isEqual(toResource(editor, { supportSideBySide: SideBySideEditor.MASTER }), resource))) { - const model = textFileService.models.get(resource); + const model = textFileService.files.get(resource); if (!model?.isReadonly()) { await textFileService.save(resource, options); } diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 1fc9899d8d..3d5e8debf6 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -315,7 +315,7 @@ configurationRegistry.registerConfiguration({ 'files.hotExit': hotExitConfiguration, 'files.defaultLanguage': { 'type': 'string', - 'description': nls.localize('defaultLanguage', "The default language mode that is assigned to new files.") + 'description': nls.localize('defaultLanguage', "The default language mode that is assigned to new files. If configured to `${activeEditorLanguage}`, will use the language mode of the currently active text editor if any.") }, 'files.maxMemoryForLargeFilesMB': { 'type': 'number', @@ -327,7 +327,7 @@ configurationRegistry.registerConfiguration({ 'type': 'boolean', 'description': nls.localize('files.preventSaveConflicts', "When enabled, will prevent to save a file that has been changed since it was last edited. Instead, a diff editor is provided to compare the changes and accept or revert them. This setting should only be disabled if you frequently encounter save conflict errors and may result in data loss if used without caution."), 'default': true, - 'scope': ConfigurationScope.RESOURCE + 'scope': ConfigurationScope.RESOURCE_LANGUAGE }, 'files.simpleDialog.enable': { 'type': 'boolean', @@ -345,12 +345,6 @@ configurationRegistry.registerConfiguration({ 'default': false, 'description': nls.localize('formatOnSave', "Format a file on save. A formatter must be available, the file must not be saved after delay, and the editor must not be shutting down."), scope: ConfigurationScope.RESOURCE_LANGUAGE, - }, - 'editor.formatOnSaveTimeout': { - 'type': 'number', - 'default': 750, - 'description': nls.localize('formatOnSaveTimeout', "Timeout in milliseconds after which the formatting that is run on file save is cancelled."), - scope: ConfigurationScope.RESOURCE_LANGUAGE, } } }); diff --git a/src/vs/workbench/contrib/files/browser/files.ts b/src/vs/workbench/contrib/files/browser/files.ts index 23200282c9..e3d0341815 100644 --- a/src/vs/workbench/contrib/files/browser/files.ts +++ b/src/vs/workbench/contrib/files/browser/files.ts @@ -57,7 +57,7 @@ export function getMultiSelectedResources(resource: URI | object | undefined, li const list = listService.lastFocusedList; if (list?.getHTMLElement() === document.activeElement) { // Explorer - if (list instanceof AsyncDataTree) { + if (list instanceof AsyncDataTree && list.getFocus().every(item => item instanceof ExplorerItem)) { // Explorer const context = explorerService.getContext(true); if (context.length) { diff --git a/src/vs/workbench/contrib/files/browser/views/emptyView.ts b/src/vs/workbench/contrib/files/browser/views/emptyView.ts index d44673863b..96ca3f2e4f 100644 --- a/src/vs/workbench/contrib/files/browser/views/emptyView.ts +++ b/src/vs/workbench/contrib/files/browser/views/emptyView.ts @@ -37,7 +37,7 @@ export class EmptyView extends ViewPane { constructor( options: IViewletViewOptions, @IThemeService private readonly themeService: IThemeService, - @IInstantiationService private readonly instantiationService: IInstantiationService, + @IInstantiationService instantiationService: IInstantiationService, @IKeybindingService keybindingService: IKeybindingService, @IContextMenuService contextMenuService: IContextMenuService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @@ -46,7 +46,7 @@ export class EmptyView extends ViewPane { @ILabelService private labelService: ILabelService, @IContextKeyService contextKeyService: IContextKeyService ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('explorerSection', "Files Explorer Section") }, keybindingService, contextMenuService, configurationService, contextKeyService); + super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('explorerSection', "Files Explorer Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, instantiationService); this._register(this.contextService.onDidChangeWorkbenchState(() => this.setLabels())); this._register(this.labelService.onDidChangeFormatters(() => this.setLabels())); } @@ -130,7 +130,7 @@ export class EmptyView extends ViewPane { } } - layoutBody(size: number): void { + layoutBody(_size: number): void { // no-op } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerDecorationsProvider.ts b/src/vs/workbench/contrib/files/browser/views/explorerDecorationsProvider.ts index f231abad56..329188c558 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerDecorationsProvider.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerDecorationsProvider.ts @@ -12,6 +12,25 @@ import { listInvalidItemForeground } from 'vs/platform/theme/common/colorRegistr import { DisposableStore } from 'vs/base/common/lifecycle'; import { IExplorerService } from 'vs/workbench/contrib/files/common/files'; import { explorerRootErrorEmitter } from 'vs/workbench/contrib/files/browser/views/explorerViewer'; +import { ExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; + +export function provideDecorations(fileStat: ExplorerItem | null): IDecorationData | undefined { + if (fileStat && fileStat.isRoot && fileStat.isError) { + return { + tooltip: localize('canNotResolve', "Can not resolve workspace folder"), + letter: '!', + color: listInvalidItemForeground, + }; + } + if (fileStat && fileStat.isSymbolicLink) { + return { + tooltip: localize('symbolicLlink', "Symbolic Link"), + letter: '\u2937' + }; + } + + return undefined; +} export class ExplorerDecorationsProvider implements IDecorationsProvider { readonly label: string = localize('label', "Explorer"); @@ -42,21 +61,7 @@ export class ExplorerDecorationsProvider implements IDecorationsProvider { provideDecorations(resource: URI): IDecorationData | undefined { const fileStat = this.explorerService.findClosest(resource); - if (fileStat && fileStat.isRoot && fileStat.isError) { - return { - tooltip: localize('canNotResolve', "Can not resolve workspace folder"), - letter: '!', - color: listInvalidItemForeground, - }; - } - if (fileStat && fileStat.isSymbolicLink) { - return { - tooltip: localize('symbolicLlink', "Symbolic Link"), - letter: '\u2937' - }; - } - - return undefined; + return provideDecorations(fileStat); } dispose(): void { diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 7415b42131..e81a9f9181 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -64,6 +64,62 @@ interface IExplorerViewStyles { listDropBackground?: Color; } +function hasExpandedRootChild(tree: WorkbenchCompressibleAsyncDataTree, treeInput: ExplorerItem[]): boolean { + for (const folder of treeInput) { + if (tree.hasNode(folder) && !tree.isCollapsed(folder)) { + for (const [, child] of folder.children.entries()) { + if (tree.hasNode(child) && !tree.isCollapsed(child)) { + return true; + } + } + } + } + + return false; +} + +export function getContext(focus: ExplorerItem[], selection: ExplorerItem[], respectMultiSelection: boolean, + compressedNavigationControllerProvider: { getCompressedNavigationController(stat: ExplorerItem): ICompressedNavigationController | undefined }): ExplorerItem[] { + + let focusedStat: ExplorerItem | undefined; + focusedStat = focus.length ? focus[0] : undefined; + + const compressedNavigationController = focusedStat && compressedNavigationControllerProvider.getCompressedNavigationController(focusedStat); + focusedStat = compressedNavigationController ? compressedNavigationController.current : focusedStat; + + const selectedStats: ExplorerItem[] = []; + + for (const stat of selection) { + const controller = compressedNavigationControllerProvider.getCompressedNavigationController(stat); + if (controller && focusedStat && controller === compressedNavigationController) { + if (stat === focusedStat) { + selectedStats.push(stat); + } + // Ignore stats which are selected but are part of the same compact node as the focused stat + continue; + } + + if (controller) { + selectedStats.push(...controller.items); + } else { + selectedStats.push(stat); + } + } + if (!focusedStat) { + if (respectMultiSelection) { + return selectedStats; + } else { + return []; + } + } + + if (respectMultiSelection && selectedStats.indexOf(focusedStat) >= 0) { + return selectedStats; + } + + return [focusedStat]; +} + export class ExplorerView extends ViewPane { static readonly ID: string = 'workbench.explorer.fileView'; static readonly TREE_VIEW_STATE_STORAGE_KEY: string = 'workbench.explorer.treeViewState'; @@ -93,7 +149,7 @@ export class ExplorerView extends ViewPane { constructor( options: IViewPaneOptions, @IContextMenuService contextMenuService: IContextMenuService, - @IInstantiationService private readonly instantiationService: IInstantiationService, + @IInstantiationService instantiationService: IInstantiationService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IProgressService private readonly progressService: IProgressService, @IEditorService private readonly editorService: IEditorService, @@ -111,7 +167,7 @@ export class ExplorerView extends ViewPane { @IClipboardService private clipboardService: IClipboardService, @IFileService private readonly fileService: IFileService ) { - super({ ...(options as IViewPaneOptions), id: ExplorerView.ID, ariaHeaderLabel: nls.localize('explorerSection', "Files Explorer Section") }, keybindingService, contextMenuService, configurationService, contextKeyService); + super({ ...(options as IViewPaneOptions), id: ExplorerView.ID, ariaHeaderLabel: nls.localize('explorerSection', "Files Explorer Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, instantiationService); this.resourceContext = instantiationService.createInstance(ResourceContextKey); this._register(this.resourceContext); @@ -139,7 +195,7 @@ export class ExplorerView extends ViewPane { return this.name; } - set title(value: string) { + set title(_: string) { // noop } @@ -283,45 +339,7 @@ export class ExplorerView extends ViewPane { } getContext(respectMultiSelection: boolean): ExplorerItem[] { - let focusedStat: ExplorerItem | undefined; - - const focus = this.tree.getFocus(); - focusedStat = focus.length ? focus[0] : undefined; - - const compressedNavigationController = focusedStat && this.renderer.getCompressedNavigationController(focusedStat); - focusedStat = compressedNavigationController ? compressedNavigationController.current : focusedStat; - - const selectedStats: ExplorerItem[] = []; - - for (const stat of this.tree.getSelection()) { - const controller = this.renderer.getCompressedNavigationController(stat); - if (controller && focusedStat && controller === compressedNavigationController) { - if (stat === focusedStat) { - selectedStats.push(stat); - } - // Ignore stats which are selected but are part of the same compact node as the focused stat - continue; - } - - if (controller) { - selectedStats.push(...controller.items); - } else { - selectedStats.push(stat); - } - } - if (!focusedStat) { - if (respectMultiSelection) { - return selectedStats; - } else { - return []; - } - } - - if (respectMultiSelection && selectedStats.indexOf(focusedStat) >= 0) { - return selectedStats; - } - - return [focusedStat]; + return getContext(this.tree.getFocus(), this.tree.getSelection(), respectMultiSelection, this.renderer); } private selectActiveFile(deselect?: boolean, reveal = this.autoReveal): void { @@ -570,8 +588,6 @@ export class ExplorerView extends ViewPane { return DOM.getLargestChildWidth(parentNode, childNodes); } - // private didLoad = false; - private setTreeInput(): Promise { if (!this.isBodyVisible()) { this.shouldRefresh = true; @@ -684,6 +700,17 @@ export class ExplorerView extends ViewPane { } collapseAll(): void { + const treeInput = this.tree.getInput(); + if (Array.isArray(treeInput)) { + if (hasExpandedRootChild(this.tree, treeInput)) { + treeInput.forEach(folder => { + folder.children.forEach(child => this.tree.hasNode(child) && this.tree.collapse(child, true)); + }); + + return; + } + } + this.tree.collapseAll(); } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 657144ae01..0f0031aabf 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -36,7 +36,7 @@ import { IDragAndDropData, DataTransfers } from 'vs/base/browser/dnd'; import { Schemas } from 'vs/base/common/network'; import { DesktopDragAndDropData, ExternalElementsDragAndDropData, ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; import { isMacintosh, isWeb } from 'vs/base/common/platform'; -import { IDialogService, IConfirmation, getConfirmMessage } from 'vs/platform/dialogs/common/dialogs'; +import { IDialogService, IConfirmation, getFileNamesMessage } from 'vs/platform/dialogs/common/dialogs'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; @@ -55,6 +55,7 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { isNumber } from 'vs/base/common/types'; import { domEvent } from 'vs/base/browser/event'; import { IEditableData } from 'vs/workbench/common/views'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; export class ExplorerDelegate implements IListVirtualDelegate { @@ -642,7 +643,8 @@ export class FileDragAndDrop implements ITreeDragAndDrop { @IInstantiationService private instantiationService: IInstantiationService, @ITextFileService private textFileService: ITextFileService, @IHostService private hostService: IHostService, - @IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService + @IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService, + @IWorkingCopyService private workingCopyService: IWorkingCopyService ) { this.toDispose = []; @@ -946,8 +948,8 @@ export class FileDragAndDrop implements ITreeDragAndDrop { // if the target exists and is dirty, make sure to revert it. otherwise the dirty contents // of the target file would replace the contents of the added file. since we already // confirmed the overwrite before, this is OK. - if (this.textFileService.isDirty(targetFile)) { - await this.textFileService.revertAll([targetFile], { soft: true }); + if (this.workingCopyService.isDirty(targetFile)) { + await Promise.all(this.workingCopyService.getWorkingCopies(targetFile).map(workingCopy => workingCopy.revert({ soft: true }))); } const copyTarget = joinPath(target.resource, basename(sourceFile)); @@ -971,11 +973,15 @@ export class FileDragAndDrop implements ITreeDragAndDrop { // Handle confirm setting const confirmDragAndDrop = !isCopy && this.configurationService.getValue(FileDragAndDrop.CONFIRM_DND_SETTING_KEY); if (confirmDragAndDrop) { + const message = items.length > 1 && items.every(s => s.isRoot) ? localize('confirmRootsMove', "Are you sure you want to change the order of multiple root folders in your workspace?") + : items.length > 1 ? localize('confirmMultiMove', "Are you sure you want to move the following {0} files into '{1}'?", items.length, target.name) + : items[0].isRoot ? localize('confirmRootMove', "Are you sure you want to change the order of root folder '{0}' in your workspace?", items[0].name) + : localize('confirmMove', "Are you sure you want to move '{0}' into '{1}'?", items[0].name, target.name); + const detail = items.length > 1 && !items.every(s => s.isRoot) ? getFileNamesMessage(items.map(i => i.resource)) : undefined; + const confirmation = await this.dialogService.confirm({ - message: items.length > 1 && items.every(s => s.isRoot) ? localize('confirmRootsMove', "Are you sure you want to change the order of multiple root folders in your workspace?") - : items.length > 1 ? getConfirmMessage(localize('confirmMultiMove', "Are you sure you want to move the following {0} files into '{1}'?", items.length, target.name), items.map(s => s.resource)) - : items[0].isRoot ? localize('confirmRootMove', "Are you sure you want to change the order of root folder '{0}' in your workspace?", items[0].name) - : localize('confirmMove', "Are you sure you want to move '{0}' into '{1}'?", items[0].name, target.name), + message, + detail, checkbox: { label: localize('doNotAskAgain', "Do not ask me again") }, diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 05a6e1539a..082cb80bc4 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -67,7 +67,7 @@ export class OpenEditorsView extends ViewPane { constructor( options: IViewletViewOptions, - @IInstantiationService private readonly instantiationService: IInstantiationService, + @IInstantiationService instantiationService: IInstantiationService, @IContextMenuService contextMenuService: IContextMenuService, @IEditorService private readonly editorService: IEditorService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @@ -83,7 +83,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); + }, keybindingService, contextMenuService, configurationService, contextKeyService, instantiationService); this.structuralRefreshDelay = 0; this.listRefreshScheduler = new RunOnceScheduler(() => { @@ -199,7 +199,7 @@ export class OpenEditorsView extends ViewPane { this.updateDirtyIndicator(); } - public renderBody(container: HTMLElement): void { + renderBody(container: HTMLElement): void { dom.addClass(container, 'explorer-open-editors'); dom.addClass(container, 'show-file-icons'); @@ -249,7 +249,7 @@ export class OpenEditorsView extends ViewPane { const element = e.elements.length ? e.elements[0] : undefined; if (element instanceof OpenEditor) { const resource = element.getResource(); - this.dirtyEditorFocusedContext.set(element.editor.isDirty()); + this.dirtyEditorFocusedContext.set(element.editor.isDirty() && !element.editor.isSaving()); this.readonlyEditorFocusedContext.set(element.editor.isReadonly()); this.resourceContext.set(withUndefinedAsNull(resource)); } else if (!!element) { @@ -299,7 +299,7 @@ export class OpenEditorsView extends ViewPane { })); } - public getActions(): IAction[] { + getActions(): IAction[] { return [ this.instantiationService.createInstance(ToggleEditorLayoutAction, ToggleEditorLayoutAction.ID, ToggleEditorLayoutAction.LABEL), this.instantiationService.createInstance(SaveAllAction, SaveAllAction.ID, SaveAllAction.LABEL), @@ -307,12 +307,12 @@ export class OpenEditorsView extends ViewPane { ]; } - public focus(): void { + focus(): void { super.focus(); this.list.domFocus(); } - public getList(): WorkbenchList { + getList(): WorkbenchList { return this.list; } @@ -419,7 +419,7 @@ export class OpenEditorsView extends ViewPane { private updateDirtyIndicator(workingCopy?: IWorkingCopy): void { if (workingCopy) { const gotDirty = workingCopy.isDirty(); - if (gotDirty && !!(workingCopy.capabilities & WorkingCopyCapabilities.AutoSave) && this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) { + if (gotDirty && !(workingCopy.capabilities & WorkingCopyCapabilities.Untitled) && this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) { return; // do not indicate dirty of working copies that are auto saved after short delay } } @@ -456,11 +456,11 @@ export class OpenEditorsView extends ViewPane { return itemsToShow * OpenEditorsDelegate.ITEM_HEIGHT; } - public setStructuralRefreshDelay(delay: number): void { + setStructuralRefreshDelay(delay: number): void { this.structuralRefreshDelay = delay; } - public getOptimalWidth(): number { + getOptimalWidth(): number { let parentNode = this.list.getHTMLElement(); let childNodes: HTMLElement[] = [].slice.call(parentNode.querySelectorAll('.open-editor > a')); @@ -498,7 +498,7 @@ class OpenEditorsDelegate implements IListVirtualDelegate this.onDirtyStateChange(e))); - this._register(this.textFileService.models.onModelSaveError(e => this.onDirtyStateChange(e))); - this._register(this.textFileService.models.onModelSaved(e => this.onDirtyStateChange(e))); - this._register(this.textFileService.models.onModelReverted(e => this.onDirtyStateChange(e))); - this._register(this.textFileService.models.onModelOrphanedChanged(e => this.onModelOrphanedChanged(e))); + // Dirty changes + this._register(this.textFileService.files.onDidChangeDirty(m => this.onDirtyStateChange(m))); + this._register(this.textFileService.files.onDidSaveError(m => this.onDirtyStateChange(m))); + this._register(this.textFileService.files.onDidSave(e => this.onDirtyStateChange(e.model))); + this._register(this.textFileService.files.onDidRevert(m => this.onDirtyStateChange(m))); + + // Label changes this._register(this.labelService.onDidChangeFormatters(() => FileEditorInput.MEMOIZER.clear())); this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(() => FileEditorInput.MEMOIZER.clear())); + this._register(this.textFileService.files.onDidChangeOrphaned(model => this.onModelOrphanedChanged(model))); } - private onDirtyStateChange(e: TextFileModelChangeEvent): void { - if (e.resource.toString() === this.resource.toString()) { + private onDirtyStateChange(model: ITextFileEditorModel): void { + if (model.resource.toString() === this.resource.toString()) { this._onDidChangeDirty.fire(); } } - private onModelOrphanedChanged(e: TextFileModelChangeEvent): void { - if (e.resource.toString() === this.resource.toString()) { + private onModelOrphanedChanged(model: ITextFileEditorModel): void { + if (model.resource.toString() === this.resource.toString()) { FileEditorInput.MEMOIZER.clear(); this._onDidChangeLabel.fire(); } } getEncoding(): string | undefined { - const textModel = this.textFileService.models.get(this.resource); + const textModel = this.textFileService.files.get(this.resource); if (textModel) { return textModel.getEncoding(); } @@ -108,7 +110,7 @@ export class FileEditorInput extends TextEditorInput implements IFileEditorInput setEncoding(encoding: string, mode: EncodingMode): void { this.setPreferredEncoding(encoding); - const textModel = this.textFileService.models.get(this.resource); + const textModel = this.textFileService.files.get(this.resource); if (textModel) { textModel.setEncoding(encoding, mode); } @@ -126,7 +128,7 @@ export class FileEditorInput extends TextEditorInput implements IFileEditorInput setMode(mode: string): void { this.setPreferredMode(mode); - const textModel = this.textFileService.models.get(this.resource); + const textModel = this.textFileService.files.get(this.resource); if (textModel) { textModel.setMode(mode); } @@ -210,7 +212,7 @@ export class FileEditorInput extends TextEditorInput implements IFileEditorInput } private decorateLabel(label: string): string { - const model = this.textFileService.models.get(this.resource); + const model = this.textFileService.files.get(this.resource); if (model?.hasState(ModelState.ORPHAN)) { return localize('orphanedFile', "{0} (deleted)", label); @@ -224,26 +226,40 @@ export class FileEditorInput extends TextEditorInput implements IFileEditorInput } isReadonly(): boolean { - const model = this.textFileService.models.get(this.resource); + const model = this.textFileService.files.get(this.resource); return model?.isReadonly() || this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly); } isDirty(): boolean { - const model = this.textFileService.models.get(this.resource); + const model = this.textFileService.files.get(this.resource); if (!model) { return false; } - if (model.hasState(ModelState.CONFLICT) || model.hasState(ModelState.ERROR)) { - return true; // always indicate dirty state if we are in conflict or error state + return model.isDirty(); + } + + isSaving(): boolean { + const model = this.textFileService.files.get(this.resource); + if (!model) { + return false; } + if (model.hasState(ModelState.SAVED) || model.hasState(ModelState.CONFLICT) || model.hasState(ModelState.ERROR)) { + return false; // require the model to be dirty and not in conflict or error state + } + + // Note: currently not checking for ModelState.PENDING_SAVE for a reason + // because we currently miss an event for this state change on editors + // and it could result in bad UX where an editor can be closed even though + // it shows up as dirty and has not finished saving yet. + if (this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) { - return false; // fast auto save enabled so we do not declare dirty + return true; // a short auto save is configured, treat this as being saved } - return model.isDirty(); + return false; } revert(options?: IRevertOptions): Promise { @@ -269,7 +285,7 @@ export class FileEditorInput extends TextEditorInput implements IFileEditorInput // Resolve as text try { - await this.textFileService.models.loadOrCreate(this.resource, { + await this.textFileService.files.resolve(this.resource, { mode: this.preferredMode, encoding: this.preferredEncoding, reload: { async: true }, // trigger a reload of the model if it exists already but do not wait to show the model @@ -279,7 +295,7 @@ export class FileEditorInput extends TextEditorInput implements IFileEditorInput // This is a bit ugly, because we first resolve the model and then resolve a model reference. the reason being that binary // or very large files do not resolve to a text file model but should be opened as binary files without text. First calling into - // loadOrCreate ensures we are not creating model references for these kind of resources. + // resolve() ensures we are not creating model references for these kind of resources. // In addition we have a bit of payload to take into account (encoding, reload) that the text resolver does not handle yet. if (!this.textModelReference) { this.textModelReference = this.textModelResolverService.createModelReference(this.resource); @@ -308,7 +324,7 @@ export class FileEditorInput extends TextEditorInput implements IFileEditorInput } isResolved(): boolean { - return !!this.textFileService.models.get(this.resource); + return !!this.textFileService.files.get(this.resource); } dispose(): void { diff --git a/src/vs/workbench/contrib/files/common/files.ts b/src/vs/workbench/contrib/files/common/files.ts index dfafed08ce..f338977629 100644 --- a/src/vs/workbench/contrib/files/common/files.ts +++ b/src/vs/workbench/contrib/files/common/files.ts @@ -246,6 +246,10 @@ export class OpenEditor implements IEditorIdentifier { return this.editor.isDirty(); } + isSaving(): boolean { + return this.editor.isSaving(); + } + getResource(): URI | undefined { return toResource(this.editor, { supportSideBySide: SideBySideEditor.MASTER }); } diff --git a/src/vs/workbench/contrib/files/electron-browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/electron-browser/fileActions.contribution.ts index 085654bf16..9dd8680a4a 100644 --- a/src/vs/workbench/contrib/files/electron-browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/electron-browser/fileActions.contribution.ts @@ -27,7 +27,7 @@ import { ShowOpenedFileInNewWindow } from 'vs/workbench/contrib/files/browser/fi import { IExplorerService } from 'vs/workbench/contrib/files/common/files'; const REVEAL_IN_OS_COMMAND_ID = 'revealFileInOS'; -const REVEAL_IN_OS_LABEL = isWindows ? nls.localize('revealInWindows', "Reveal in Explorer") : isMacintosh ? nls.localize('revealInMac', "Reveal in Finder") : nls.localize('openContainer', "Open Containing Folder"); +const REVEAL_IN_OS_LABEL = isWindows ? nls.localize('revealInWindows', "Reveal in File Explorer") : isMacintosh ? nls.localize('revealInMac', "Reveal in Finder") : nls.localize('openContainer', "Open Containing Folder"); KeybindingsRegistry.registerCommandAndKeybindingRule({ id: REVEAL_IN_OS_COMMAND_ID, @@ -63,7 +63,7 @@ appendEditorTitleContextMenuItem(REVEAL_IN_OS_COMMAND_ID, REVEAL_IN_OS_LABEL, Re const revealInOsCommand = { id: REVEAL_IN_OS_COMMAND_ID, - title: isWindows ? nls.localize('revealInWindows', "Reveal in Explorer") : isMacintosh ? nls.localize('revealInMac', "Reveal in Finder") : nls.localize('openContainer', "Open Containing Folder") + title: isWindows ? nls.localize('revealInWindows', "Reveal in File Explorer") : isMacintosh ? nls.localize('revealInMac', "Reveal in Finder") : nls.localize('openContainer', "Open Containing Folder") }; MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { group: 'navigation', @@ -84,7 +84,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { // Command Palette const category = { value: nls.localize('filesCategory', "File"), original: 'File' }; -appendToCommandPalette(REVEAL_IN_OS_COMMAND_ID, { value: REVEAL_IN_OS_LABEL, original: isWindows ? 'Reveal in Explorer' : isMacintosh ? 'Reveal in Finder' : 'Open Containing Folder' }, category); +appendToCommandPalette(REVEAL_IN_OS_COMMAND_ID, { value: REVEAL_IN_OS_LABEL, original: isWindows ? 'Reveal in File Explorer' : isMacintosh ? 'Reveal in Finder' : 'Open Containing Folder' }, category); const registry = Registry.as(ActionExtensions.WorkbenchActions); registry.registerWorkbenchAction(SyncActionDescriptor.create(ShowOpenedFileInNewWindow, ShowOpenedFileInNewWindow.ID, ShowOpenedFileInNewWindow.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_O) }), 'File: Open Active File in New Window', category.value); diff --git a/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts b/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts new file mode 100644 index 0000000000..cc41cbf695 --- /dev/null +++ b/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts @@ -0,0 +1,110 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +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 { workbenchInstantiationService, TestTextFileService, TestFileService, TestFilesConfigurationService, TestEnvironmentService } from 'vs/workbench/test/workbenchTestServices'; +import { ITextFileService, IResolvedTextFileEditorModel, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; +import { IFileService } from 'vs/platform/files/common/files'; +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'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { TextFileEditor } from 'vs/workbench/contrib/files/browser/editors/textFileEditor'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { EditorInput } from 'vs/workbench/common/editor'; +import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; +import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; +import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; +import { EditorAutoSave } from 'vs/workbench/browser/parts/editor/editorAutoSave'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +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: TestTextFileService, + @IFileService public fileService: TestFileService, + @IConfigurationService public configurationService: TestConfigurationService + ) { + } +} + +suite('EditorAutoSave', () => { + + let disposables: IDisposable[] = []; + + setup(() => { + disposables.push(Registry.as(EditorExtensions.Editors).registerEditor( + EditorDescriptor.create( + TextFileEditor, + TextFileEditor.ID, + 'Text File Editor' + ), + [new SyncDescriptor(FileEditorInput)] + )); + }); + + teardown(() => { + dispose(disposables); + disposables = []; + }); + + test('editor auto saves after short delay if configured', async function () { + const instantiationService = workbenchInstantiationService(); + + const configurationService = new TestConfigurationService(); + configurationService.setUserConfiguration('files', { autoSave: 'afterDelay', autoSaveDelay: 1 }); + instantiationService.stub(IConfigurationService, configurationService); + + instantiationService.stub(IFilesConfigurationService, new TestFilesConfigurationService( + instantiationService.createInstance(MockContextKeyService), + configurationService, + TestEnvironmentService + )); + + const part = instantiationService.createInstance(EditorPart); + part.create(document.createElement('div')); + part.layout(400, 300); + + instantiationService.stub(IEditorGroupsService, part); + + const editorService: EditorService = instantiationService.createInstance(EditorService); + instantiationService.stub(IEditorService, editorService); + + const accessor = instantiationService.createInstance(ServiceAccessor); + + const editorAutoSave = instantiationService.createInstance(EditorAutoSave); + + const resource = toResource.call(this, '/path/index.txt'); + + const model = await accessor.textFileService.files.resolve(resource) as IResolvedTextFileEditorModel; + + model.textEditorModel.setValue('Super Good'); + + assert.ok(model.isDirty()); + + await awaitModelSaved(model); + + assert.ok(!model.isDirty()); + + part.dispose(); + editorAutoSave.dispose(); + (accessor.textFileService.files).dispose(); + }); + + function awaitModelSaved(model: ITextFileEditorModel): Promise { + return new Promise(c => { + Event.once(model.onDidChangeDirty)(c); + }); + } +}); diff --git a/src/vs/workbench/contrib/files/test/browser/explorerView.test.ts b/src/vs/workbench/contrib/files/test/browser/explorerView.test.ts new file mode 100644 index 0000000000..a78c8a0443 --- /dev/null +++ b/src/vs/workbench/contrib/files/test/browser/explorerView.test.ts @@ -0,0 +1,107 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { Emitter } from 'vs/base/common/event'; +import { toResource } from 'vs/base/test/common/utils'; +import { TestFileService } from 'vs/workbench/test/workbenchTestServices'; +import { ExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; +import { getContext } from 'vs/workbench/contrib/files/browser/views/explorerView'; +import { listInvalidItemForeground } from 'vs/platform/theme/common/colorRegistry'; +import { CompressedNavigationController } from 'vs/workbench/contrib/files/browser/views/explorerViewer'; +import * as dom from 'vs/base/browser/dom'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { provideDecorations } from 'vs/workbench/contrib/files/browser/views/explorerDecorationsProvider'; +const $ = dom.$; + +const fileService = new TestFileService(); + +function createStat(this: any, path: string, name: string, isFolder: boolean, hasChildren: boolean, size: number, mtime: number, isSymLink = false): ExplorerItem { + return new ExplorerItem(toResource.call(this, path), fileService, undefined, isFolder, isSymLink, name, mtime); +} + +suite('Files - ExplorerView', () => { + + test('getContext', async function () { + const d = new Date().getTime(); + const s1 = createStat.call(this, '/', '/', true, false, 8096, d); + const s2 = createStat.call(this, '/path', 'path', true, false, 8096, d); + const s3 = createStat.call(this, '/path/to', 'to', true, false, 8096, d); + const s4 = createStat.call(this, '/path/to/stat', 'stat', false, false, 8096, d); + const noNavigationController = { getCompressedNavigationController: (stat: ExplorerItem) => undefined }; + + assert.deepEqual(getContext([s1], [s2, s3, s4], true, noNavigationController), [s1]); + assert.deepEqual(getContext([s1], [s1, s3, s4], true, noNavigationController), [s1, s3, s4]); + assert.deepEqual(getContext([s1], [s3, s1, s4], false, noNavigationController), [s1]); + assert.deepEqual(getContext([], [s3, s1, s4], false, noNavigationController), []); + assert.deepEqual(getContext([], [s3, s1, s4], true, noNavigationController), [s3, s1, s4]); + }); + + test('decoration provider', async function () { + const d = new Date().getTime(); + const s1 = createStat.call(this, '/path', 'path', true, false, 8096, d); + s1.isError = true; + const s2 = createStat.call(this, '/path/to', 'to', true, false, 8096, d, true); + const s3 = createStat.call(this, '/path/to/stat', 'stat', false, false, 8096, d); + assert.equal(provideDecorations(s3), undefined); + assert.deepEqual(provideDecorations(s2), { + tooltip: 'Symbolic Link', + letter: '\u2937' + }); + assert.deepEqual(provideDecorations(s1), { + tooltip: 'Can not resolve workspace folder', + letter: '!', + color: listInvalidItemForeground + }); + + }); + + test('compressed navigation controller', async function () { + const container = $('.file'); + const label = $('.label'); + const labelName1 = $('.label-name'); + const labelName2 = $('.label-name'); + const labelName3 = $('.label-name'); + const d = new Date().getTime(); + const s1 = createStat.call(this, '/path', 'path', true, false, 8096, d); + const s2 = createStat.call(this, '/path/to', 'to', true, false, 8096, d); + const s3 = createStat.call(this, '/path/to/stat', 'stat', false, false, 8096, d); + + dom.append(container, label); + dom.append(label, labelName1); + dom.append(label, labelName2); + dom.append(label, labelName3); + const emitter = new Emitter(); + + const navigationController = new CompressedNavigationController('id', [s1, s2, s3], { + container, + elementDisposable: Disposable.None, + label: { + container: label, + onDidRender: emitter.event + } + }); + + assert.equal(navigationController.count, 3); + assert.equal(navigationController.index, 2); + assert.equal(navigationController.current, s3); + navigationController.next(); + assert.equal(navigationController.current, s3); + navigationController.previous(); + assert.equal(navigationController.current, s2); + navigationController.previous(); + assert.equal(navigationController.current, s1); + navigationController.previous(); + assert.equal(navigationController.current, s1); + navigationController.last(); + assert.equal(navigationController.current, s3); + navigationController.first(); + assert.equal(navigationController.current, s1); + navigationController.setIndex(1); + assert.equal(navigationController.current, s2); + navigationController.setIndex(44); + assert.equal(navigationController.current, s2); + }); +}); 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 d3ea4e5255..2e1b052161 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileEditorTracker.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileEditorTracker.test.ts @@ -4,15 +4,25 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { Event } from 'vs/base/common/event'; import { FileEditorTracker } from 'vs/workbench/contrib/files/browser/editors/fileEditorTracker'; import { toResource } from 'vs/base/test/common/utils'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { workbenchInstantiationService, TestTextFileService, TestFileService } from 'vs/workbench/test/workbenchTestServices'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITextFileService, IResolvedTextFileEditorModel, snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; import { FileChangesEvent, FileChangeType, IFileService } 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'; +import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { TextFileEditor } from 'vs/workbench/contrib/files/browser/editors/textFileEditor'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { EditorInput } from 'vs/workbench/common/editor'; +import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; +import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; +import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; class ServiceAccessor { constructor( @@ -26,20 +36,33 @@ class ServiceAccessor { suite('Files - FileEditorTracker', () => { - let instantiationService: IInstantiationService; - let accessor: ServiceAccessor; + let disposables: IDisposable[] = []; setup(() => { - instantiationService = workbenchInstantiationService(); - accessor = instantiationService.createInstance(ServiceAccessor); + disposables.push(Registry.as(EditorExtensions.Editors).registerEditor( + EditorDescriptor.create( + TextFileEditor, + TextFileEditor.ID, + 'Text File Editor' + ), + [new SyncDescriptor(FileEditorInput)] + )); }); - test('file change event updates model', async function () { + teardown(() => { + dispose(disposables); + disposables = []; + }); + + test.skip('file change event updates model', async function () { // {{SQL CARBON EDIT}} tabcolormode failure + const instantiationService = workbenchInstantiationService(); + const accessor = instantiationService.createInstance(ServiceAccessor); + const tracker = instantiationService.createInstance(FileEditorTracker); const resource = toResource.call(this, '/path/index.txt'); - const model = await accessor.textFileService.models.loadOrCreate(resource) as IResolvedTextFileEditorModel; + const model = await accessor.textFileService.files.resolve(resource) as IResolvedTextFileEditorModel; model.textEditorModel.setValue('Super Good'); assert.equal(snapshotToString(model.createSnapshot()!), 'Super Good'); @@ -54,5 +77,70 @@ suite('Files - FileEditorTracker', () => { assert.equal(snapshotToString(model.createSnapshot()!), 'Hello Html'); tracker.dispose(); + (accessor.textFileService.files).dispose(); }); + + async function createTracker(): Promise<[EditorPart, ServiceAccessor, FileEditorTracker]> { + const instantiationService = workbenchInstantiationService(); + + const part = instantiationService.createInstance(EditorPart); + part.create(document.createElement('div')); + part.layout(400, 300); + + instantiationService.stub(IEditorGroupsService, part); + + const editorService: EditorService = instantiationService.createInstance(EditorService); + instantiationService.stub(IEditorService, editorService); + + const accessor = instantiationService.createInstance(ServiceAccessor); + + await part.whenRestored; + + const tracker = instantiationService.createInstance(FileEditorTracker); + + return [part, accessor, tracker]; + } + + test.skip('dirty text file model opens as editor', async function () { // {{SQL CARBON EDIT}} tabcolormode failure + const [part, accessor, tracker] = await createTracker(); + + const resource = toResource.call(this, '/path/index.txt'); + + assert.ok(!accessor.editorService.isOpen({ resource })); + + const model = await accessor.textFileService.files.resolve(resource) as IResolvedTextFileEditorModel; + + model.textEditorModel.setValue('Super Good'); + + await awaitEditorOpening(accessor.editorService); + assert.ok(accessor.editorService.isOpen({ resource })); + + part.dispose(); + tracker.dispose(); + (accessor.textFileService.files).dispose(); + }); + + test.skip('dirty untitled text file model opens as editor', async function () { // {{SQL CARBON EDIT}} tabcolormode failure + const [part, accessor, tracker] = await createTracker(); + + const untitledEditor = accessor.textFileService.untitled.create(); + const model = await untitledEditor.resolve(); + + assert.ok(!accessor.editorService.isOpen(untitledEditor)); + + model.textEditorModel.setValue('Super Good'); + + await awaitEditorOpening(accessor.editorService); + assert.ok(accessor.editorService.isOpen(untitledEditor)); + + part.dispose(); + tracker.dispose(); + model.dispose(); + }); + + function awaitEditorOpening(editorService: IEditorService): Promise { + return new Promise(c => { + Event.once(editorService.onDidActiveEditorChange)(c); + }); + } }); diff --git a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts index 61fd46e5e4..af6411b893 100644 --- a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts +++ b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts @@ -13,7 +13,7 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { localize } from 'vs/nls'; import { Marker, RelatedInformation } from 'vs/workbench/contrib/markers/browser/markersModel'; import { MarkersView, getMarkersView } from 'vs/workbench/contrib/markers/browser/markersView'; -import { MenuId, MenuRegistry, SyncActionDescriptor, registerAction } from 'vs/platform/actions/common/actions'; +import { MenuId, MenuRegistry, SyncActionDescriptor, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { TogglePanelAction } from 'vs/workbench/browser/panel'; import { Registry } from 'vs/platform/registry/common/platform'; import { ShowProblemsPanelAction } from 'vs/workbench/contrib/markers/browser/markersViewActions'; @@ -32,6 +32,8 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ViewContainer, IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation, IViewsRegistry } from 'vs/workbench/common/views'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import type { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; registerSingleton(IMarkersWorkbenchService, MarkersWorkbenchService, false); @@ -110,7 +112,7 @@ class ToggleMarkersPanelAction extends TogglePanelAction { const VIEW_CONTAINER: ViewContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ id: Constants.MARKERS_PANEL_ID, name: Messages.MARKERS_PANEL_TITLE_PROBLEMS, - ctorDescriptor: { ctor: ViewPaneContainer, arguments: [Constants.MARKERS_PANEL_ID, Constants.MARKERS_PANEL_STORAGE_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }] }, + ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [Constants.MARKERS_PANEL_ID, Constants.MARKERS_PANEL_STORAGE_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), focusCommand: { id: ToggleMarkersPanelAction.ID, keybindings: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_M @@ -122,7 +124,7 @@ Registry.as(ViewContainerExtensions.ViewsRegistry).registerViews id: Constants.MARKERS_VIEW_ID, name: Messages.MARKERS_PANEL_TITLE_PROBLEMS, canToggleVisibility: false, - ctorDescriptor: { ctor: MarkersView }, + ctorDescriptor: new SyncDescriptor(MarkersView), }], VIEW_CONTAINER); // workbench @@ -135,103 +137,127 @@ registry.registerWorkbenchAction(SyncActionDescriptor.create(ToggleMarkersPanelA primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_M }), 'View: Toggle Problems (Errors, Warnings, Infos)', Messages.MARKERS_PANEL_VIEW_CATEGORY); registry.registerWorkbenchAction(SyncActionDescriptor.create(ShowProblemsPanelAction, ShowProblemsPanelAction.ID, ShowProblemsPanelAction.LABEL), 'View: Focus Problems (Errors, Warnings, Infos)', Messages.MARKERS_PANEL_VIEW_CATEGORY); -registerAction({ - id: Constants.MARKER_COPY_ACTION_ID, - title: { value: localize('copyMarker', "Copy"), original: 'Copy' }, - async handler(accessor) { +registerAction2(class extends Action2 { + constructor() { + super({ + id: Constants.MARKER_COPY_ACTION_ID, + title: { value: localize('copyMarker', "Copy"), original: 'Copy' }, + menu: { + id: MenuId.ProblemsPanelContext, + when: Constants.MarkerFocusContextKey, + group: 'navigation' + }, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.KEY_C, + when: Constants.MarkerFocusContextKey + }, + }); + } + async run(accessor: ServicesAccessor) { await copyMarker(accessor.get(IPanelService), accessor.get(IClipboardService)); - }, - menu: { - menuId: MenuId.ProblemsPanelContext, - when: Constants.MarkerFocusContextKey, - group: 'navigation' - }, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - keys: { - primary: KeyMod.CtrlCmd | KeyCode.KEY_C - }, - when: Constants.MarkerFocusContextKey } }); -registerAction({ - id: Constants.MARKER_COPY_MESSAGE_ACTION_ID, - title: { value: localize('copyMessage', "Copy Message"), original: 'Copy Message' }, - async handler(accessor) { +registerAction2(class extends Action2 { + constructor() { + super({ + id: Constants.MARKER_COPY_MESSAGE_ACTION_ID, + title: { value: localize('copyMessage', "Copy Message"), original: 'Copy Message' }, + menu: { + id: MenuId.ProblemsPanelContext, + when: Constants.MarkerFocusContextKey, + group: 'navigation' + }, + }); + } + async run(accessor: ServicesAccessor) { await copyMessage(accessor.get(IPanelService), accessor.get(IClipboardService)); - }, - menu: { - menuId: MenuId.ProblemsPanelContext, - when: Constants.MarkerFocusContextKey, - group: 'navigation' } }); -registerAction({ - id: Constants.RELATED_INFORMATION_COPY_MESSAGE_ACTION_ID, - title: { value: localize('copyMessage', "Copy Message"), original: 'Copy Message' }, - async handler(accessor) { +registerAction2(class extends Action2 { + constructor() { + super({ + id: Constants.RELATED_INFORMATION_COPY_MESSAGE_ACTION_ID, + title: { value: localize('copyMessage', "Copy Message"), original: 'Copy Message' }, + menu: { + id: MenuId.ProblemsPanelContext, + when: Constants.RelatedInformationFocusContextKey, + group: 'navigation' + } + }); + } + async run(accessor: ServicesAccessor) { await copyRelatedInformationMessage(accessor.get(IPanelService), accessor.get(IClipboardService)); - }, - menu: { - menuId: MenuId.ProblemsPanelContext, - when: Constants.RelatedInformationFocusContextKey, - group: 'navigation' } }); -registerAction({ - id: Constants.FOCUS_PROBLEMS_FROM_FILTER, - handler(accessor) { +registerAction2(class extends Action2 { + constructor() { + super({ + id: Constants.FOCUS_PROBLEMS_FROM_FILTER, + title: localize('focusProblemsList', "Focus problems view"), + keybinding: { + when: Constants.MarkerPanelFilterFocusContextKey, + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.DownArrow + } + }); + } + run(accessor: ServicesAccessor) { focusProblemsView(accessor.get(IPanelService)); - }, - keybinding: { - when: Constants.MarkerPanelFilterFocusContextKey, - weight: KeybindingWeight.WorkbenchContrib, - keys: { - primary: KeyMod.CtrlCmd | KeyCode.DownArrow - }, } }); -registerAction({ - id: Constants.MARKERS_PANEL_FOCUS_FILTER, - handler(accessor) { +registerAction2(class extends Action2 { + constructor() { + super({ + id: Constants.MARKERS_PANEL_FOCUS_FILTER, + title: localize('focusProblemsFilter', "Focus problems filter"), + keybinding: { + when: Constants.MarkerPanelFocusContextKey, + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.KEY_F + } + }); + } + run(accessor: ServicesAccessor) { focusProblemsFilter(accessor.get(IPanelService)); - }, - keybinding: { - when: Constants.MarkerPanelFocusContextKey, - weight: KeybindingWeight.WorkbenchContrib, - keys: { - primary: KeyMod.CtrlCmd | KeyCode.KEY_F - }, } }); -registerAction({ - id: Constants.MARKERS_PANEL_SHOW_MULTILINE_MESSAGE, - handler(accessor) { +registerAction2(class extends Action2 { + constructor() { + super({ + id: Constants.MARKERS_PANEL_SHOW_MULTILINE_MESSAGE, + title: { value: localize('show multiline', "Show message in multiple lines"), original: 'Problems: Show message in multiple lines' }, + category: localize('problems', "Problems"), + menu: { + id: MenuId.CommandPalette, + when: ActivePanelContext.isEqualTo(Constants.MARKERS_PANEL_ID) + } + }); + } + run(accessor: ServicesAccessor) { const markersView = getMarkersView(accessor.get(IPanelService)); if (markersView) { markersView.markersViewModel.multiline = true; } - }, - title: { value: localize('show multiline', "Show message in multiple lines"), original: 'Problems: Show message in multiple lines' }, - category: localize('problems', "Problems"), - menu: { - menuId: MenuId.CommandPalette, - when: ActivePanelContext.isEqualTo(Constants.MARKERS_PANEL_ID) } }); -registerAction({ - id: Constants.MARKERS_PANEL_SHOW_SINGLELINE_MESSAGE, - handler(accessor) { +registerAction2(class extends Action2 { + constructor() { + super({ + id: Constants.MARKERS_PANEL_SHOW_SINGLELINE_MESSAGE, + title: { value: localize('show singleline', "Show message in single line"), original: 'Problems: Show message in single line' }, + category: localize('problems', "Problems"), + menu: { + id: MenuId.CommandPalette, + when: ActivePanelContext.isEqualTo(Constants.MARKERS_PANEL_ID) + } + }); + } + run(accessor: ServicesAccessor) { const markersView = getMarkersView(accessor.get(IPanelService)); if (markersView) { markersView.markersViewModel.multiline = false; } - }, - title: { value: localize('show singleline', "Show message in single line"), original: 'Problems: Show message in single line' }, - category: localize('problems', "Problems"), - menu: { - menuId: MenuId.CommandPalette, - when: ActivePanelContext.isEqualTo(Constants.MARKERS_PANEL_ID) } }); diff --git a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts index 391483267a..3703668f46 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts +++ b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts @@ -39,11 +39,10 @@ import { Range } from 'vs/editor/common/core/range'; import { getCodeActions, CodeActionSet } from 'vs/editor/contrib/codeAction/codeAction'; import { CodeActionKind } from 'vs/editor/contrib/codeAction/types'; import { ITextModel } from 'vs/editor/common/model'; -import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; -import { ICommandService } from 'vs/platform/commands/common/commands'; import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { applyCodeAction } from 'vs/editor/contrib/codeAction/codeActionCommands'; import { SeverityIcon } from 'vs/platform/severityIcon/common/severityIcon'; +import { CodeActionTriggerType } from 'vs/editor/common/modes'; export type TreeElement = ResourceMarkers | Marker | RelatedInformation; @@ -489,8 +488,6 @@ export class MarkerViewModel extends Disposable { private readonly marker: Marker, @IModelService private modelService: IModelService, @IInstantiationService private instantiationService: IInstantiationService, - @IBulkEditService private readonly bulkEditService: IBulkEditService, - @ICommandService private readonly commandService: ICommandService, @IEditorService private readonly editorService: IEditorService ) { super(); @@ -552,7 +549,9 @@ export class MarkerViewModel extends Disposable { if (model) { if (!this.codeActionsPromise) { 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: 'manual', filter: { include: CodeActionKind.QuickFix } }, cancellationToken).then(actions => { + 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 => { return this._register(actions); }); }); @@ -571,7 +570,7 @@ export class MarkerViewModel extends Disposable { true, () => { return this.openFileAtMarker(this.marker) - .then(() => this.instantiationService.invokeFunction(applyCodeAction, codeAction, this.bulkEditService, this.commandService)); + .then(() => this.instantiationService.invokeFunction(applyCodeAction, codeAction)); })); } diff --git a/src/vs/workbench/contrib/markers/browser/markersView.ts b/src/vs/workbench/contrib/markers/browser/markersView.ts index a5ace64a09..161d39f9fc 100644 --- a/src/vs/workbench/contrib/markers/browser/markersView.ts +++ b/src/vs/workbench/contrib/markers/browser/markersView.ts @@ -101,7 +101,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { constructor( options: IViewPaneOptions, - @IInstantiationService private readonly instantiationService: IInstantiationService, + @IInstantiationService instantiationService: IInstantiationService, @IEditorService private readonly editorService: IEditorService, @IConfigurationService configurationService: IConfigurationService, @ITelemetryService private readonly telemetryService: ITelemetryService, @@ -113,7 +113,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { @IKeybindingService keybindingService: IKeybindingService, @IStorageService storageService: IStorageService, ) { - super({ ...(options as IViewPaneOptions), id: Constants.MARKERS_VIEW_ID, ariaHeaderLabel: Messages.MARKERS_PANEL_TITLE_PROBLEMS }, keybindingService, contextMenuService, configurationService, contextKeyService); + super({ ...(options as IViewPaneOptions), id: Constants.MARKERS_VIEW_ID, ariaHeaderLabel: Messages.MARKERS_PANEL_TITLE_PROBLEMS }, keybindingService, contextMenuService, configurationService, contextKeyService, instantiationService); this.panelFoucusContextKey = Constants.MarkerPanelFocusContextKey.bindTo(contextKeyService); this.panelState = new Memento(Constants.MARKERS_PANEL_STORAGE_ID, storageService).getMemento(StorageScope.WORKSPACE); this.markersViewModel = this._register(instantiationService.createInstance(MarkersViewModel, this.panelState['multiline'])); diff --git a/src/vs/workbench/contrib/markers/browser/messages.ts b/src/vs/workbench/contrib/markers/browser/messages.ts index 6f3030968b..5541c63c2d 100644 --- a/src/vs/workbench/contrib/markers/browser/messages.ts +++ b/src/vs/workbench/contrib/markers/browser/messages.ts @@ -67,7 +67,7 @@ export default class Messages { return marker.marker.source ? nls.localize('problems.tree.aria.label.marker', "Problem generated by {0}: {1} at line {2} and character {3}.{4}", marker.marker.source, marker.marker.message, marker.marker.startLineNumber, marker.marker.startColumn, relatedInformationMessage) : nls.localize('problems.tree.aria.label.marker.nosource', "Problem: {0} at line {1} and character {2}.{3}", marker.marker.message, marker.marker.startLineNumber, marker.marker.startColumn, relatedInformationMessage); } - } + }; public static readonly MARKERS_TREE_ARIA_LABEL_RELATED_INFORMATION = (relatedInformation: IRelatedInformation): string => nls.localize('problems.tree.aria.label.relatedinfo.message', "{0} at line {1} and character {2} in {3}", relatedInformation.message, relatedInformation.startLineNumber, relatedInformation.startColumn, basename(relatedInformation.resource)); public static SHOW_ERRORS_WARNINGS_ACTION_LABEL: string = nls.localize('errors.warnings.show.label', "Show Errors and Warnings"); } diff --git a/src/vs/workbench/contrib/outline/browser/outline.contribution.ts b/src/vs/workbench/contrib/outline/browser/outline.contribution.ts index bea35a1970..fa1b311b75 100644 --- a/src/vs/workbench/contrib/outline/browser/outline.contribution.ts +++ b/src/vs/workbench/contrib/outline/browser/outline.contribution.ts @@ -4,19 +4,60 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { IViewsRegistry, IViewDescriptor, Extensions as ViewExtensions } from 'vs/workbench/common/views'; +import { IViewsRegistry, IViewDescriptor, Extensions as ViewExtensions, ViewContainer, IViewContainersRegistry, ViewContainerLocation, IViewDescriptorService, IViewsService } from 'vs/workbench/common/views'; import { OutlinePane } from './outlinePane'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { OutlineConfigKeys, OutlineViewId } from 'vs/editor/contrib/documentSymbols/outline'; import { VIEW_CONTAINER } from 'vs/workbench/contrib/files/browser/explorerViewlet'; +import { Action } from 'vs/base/common/actions'; +import { IWorkbenchActionRegistry, Extensions as ActionsExtensions } from 'vs/workbench/common/actions'; +import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; // import './outlineNavigation'; +export const PANEL_ID = 'panel.view.outline'; + +export class OutlineViewPaneContainer extends ViewPaneContainer { + constructor( + @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, + @ITelemetryService telemetryService: ITelemetryService, + @IWorkspaceContextService protected contextService: IWorkspaceContextService, + @IStorageService protected storageService: IStorageService, + @IConfigurationService configurationService: IConfigurationService, + @IInstantiationService protected instantiationService: IInstantiationService, + @IThemeService themeService: IThemeService, + @IContextMenuService contextMenuService: IContextMenuService, + @IExtensionService extensionService: IExtensionService, + ) { + super(PANEL_ID, `${PANEL_ID}.state`, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService); + } +} + +export const VIEW_CONTAINER_PANEL: ViewContainer = + Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ + id: PANEL_ID, + ctorDescriptor: new SyncDescriptor(OutlineViewPaneContainer), + name: localize('name', "Outline"), + hideIfEmpty: true + }, ViewContainerLocation.Panel); + + const _outlineDesc = { id: OutlineViewId, name: localize('name', "Outline"), - ctorDescriptor: { ctor: OutlinePane }, + ctorDescriptor: new SyncDescriptor(OutlinePane), canToggleVisibility: true, hideByDefault: false, collapsed: true, @@ -27,6 +68,37 @@ const _outlineDesc = { Registry.as(ViewExtensions.ViewsRegistry).registerViews([_outlineDesc], VIEW_CONTAINER); +export class ToggleOutlinePositionAction extends Action { + + static ID = 'outline.view.togglePosition'; + static LABEL = 'Toggle Outline View Position'; + + constructor( + id: string, + label: string, + @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, + @IViewsService private readonly viewsService: IViewsService + ) { + super(id, label, '', true); + } + + async run(): Promise { + const inPanel = this.viewDescriptorService.getViewContainer(_outlineDesc.id) === VIEW_CONTAINER_PANEL; + if (!inPanel) { + this.viewDescriptorService.moveViews([_outlineDesc], VIEW_CONTAINER_PANEL); + this.viewsService.openView(OutlineViewId, true); + } else { + this.viewDescriptorService.moveViews([_outlineDesc], VIEW_CONTAINER); + this.viewsService.openView(OutlineViewId, true); + } + + } +} + +Registry.as(ActionsExtensions.WorkbenchActions) + .registerWorkbenchAction(SyncActionDescriptor.create(ToggleOutlinePositionAction, ToggleOutlinePositionAction.ID, ToggleOutlinePositionAction.LABEL), 'Show Release Notes'); + + Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ 'id': 'outline', 'order': 117, diff --git a/src/vs/workbench/contrib/outline/browser/outlinePane.ts b/src/vs/workbench/contrib/outline/browser/outlinePane.ts index 2502c88885..abbefceefe 100644 --- a/src/vs/workbench/contrib/outline/browser/outlinePane.ts +++ b/src/vs/workbench/contrib/outline/browser/outlinePane.ts @@ -266,7 +266,7 @@ export class OutlinePane extends ViewPane { @IContextKeyService contextKeyService: IContextKeyService, @IContextMenuService contextMenuService: IContextMenuService, ) { - super(options, keybindingService, contextMenuService, _configurationService, contextKeyService); + super(options, keybindingService, contextMenuService, _configurationService, contextKeyService, _instantiationService); this._outlineViewState.restore(this._storageService); this._contextKeyFocused = OutlineViewFocused.bindTo(contextKeyService); this._contextKeyFiltered = OutlineViewFiltered.bindTo(contextKeyService); diff --git a/src/vs/workbench/contrib/output/browser/logViewer.ts b/src/vs/workbench/contrib/output/browser/logViewer.ts index e583b06f31..5d634cba48 100644 --- a/src/vs/workbench/contrib/output/browser/logViewer.ts +++ b/src/vs/workbench/contrib/output/browser/logViewer.ts @@ -18,22 +18,26 @@ import { LOG_SCHEME } from 'vs/workbench/contrib/output/common/output'; import { IFileOutputChannelDescriptor } from 'vs/workbench/services/output/common/output'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; export class LogViewerInput extends ResourceEditorInput { - public static readonly ID = 'workbench.editorinputs.output'; + static readonly ID = 'workbench.editorinputs.output'; - constructor(private readonly outputChannelDescriptor: IFileOutputChannelDescriptor, - @ITextModelService textModelResolverService: ITextModelService + constructor( + private readonly outputChannelDescriptor: IFileOutputChannelDescriptor, + @ITextModelService textModelResolverService: ITextModelService, + @ITextFileService textFileService: ITextFileService, + @IEditorService editorService: IEditorService ) { - super(basename(outputChannelDescriptor.file.path), dirname(outputChannelDescriptor.file.path), URI.from({ scheme: LOG_SCHEME, path: outputChannelDescriptor.id }), undefined, textModelResolverService); + super(basename(outputChannelDescriptor.file.path), dirname(outputChannelDescriptor.file.path), URI.from({ scheme: LOG_SCHEME, path: outputChannelDescriptor.id }), undefined, textModelResolverService, textFileService, editorService); } - public getTypeId(): string { + getTypeId(): string { return LogViewerInput.ID; } - public getResource(): URI { + getResource(): URI { return this.outputChannelDescriptor.file; } } diff --git a/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index 2102a266f2..a3c0bfb8b5 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; -import { MenuId, MenuRegistry, SyncActionDescriptor, registerAction } from 'vs/platform/actions/common/actions'; +import { MenuId, MenuRegistry, SyncActionDescriptor, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { OutputService, LogContentProvider } from 'vs/workbench/contrib/output/browser/outputServices'; @@ -20,7 +20,7 @@ import { LogViewer, LogViewerInput } from 'vs/workbench/contrib/output/browser/l import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; // Register Service @@ -89,14 +89,18 @@ actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ShowLogsOutpu actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(OpenOutputLogFileAction, OpenOutputLogFileAction.ID, OpenOutputLogFileAction.LABEL), 'Developer: Open Log File...', devCategory); // Define clear command, contribute to editor context menu -registerAction({ - id: 'editor.action.clearoutput', - title: { value: nls.localize('clearOutput.label', "Clear Output"), original: 'Clear Output' }, - menu: { - menuId: MenuId.EditorContext, - when: CONTEXT_IN_OUTPUT - }, - handler(accessor) { +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'editor.action.clearoutput', + title: { value: nls.localize('clearOutput.label', "Clear Output"), original: 'Clear Output' }, + menu: { + id: MenuId.EditorContext, + when: CONTEXT_IN_OUTPUT + }, + }); + } + run(accessor: ServicesAccessor) { const activeChannel = accessor.get(IOutputService).getActiveChannel(); if (activeChannel) { activeChannel.clear(); @@ -104,14 +108,18 @@ registerAction({ } }); -registerAction({ - id: 'workbench.action.openActiveLogOutputFile', - title: { value: nls.localize('openActiveLogOutputFile', "Open Active Log Output File"), original: 'Open Active Log Output File' }, - menu: { - menuId: MenuId.CommandPalette, - when: CONTEXT_ACTIVE_LOG_OUTPUT - }, - handler(accessor) { +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.openActiveLogOutputFile', + title: { value: nls.localize('openActiveLogOutputFile', "Open Active Log Output File"), original: 'Open Active Log Output File' }, + menu: { + id: MenuId.CommandPalette, + when: CONTEXT_ACTIVE_LOG_OUTPUT + }, + }); + } + run(accessor: ServicesAccessor) { accessor.get(IInstantiationService).createInstance(OpenLogOutputFile).run(); } }); diff --git a/src/vs/workbench/contrib/output/browser/outputActions.ts b/src/vs/workbench/contrib/output/browser/outputActions.ts index 6bbf67b15b..ed318c0453 100644 --- a/src/vs/workbench/contrib/output/browser/outputActions.ts +++ b/src/vs/workbench/contrib/output/browser/outputActions.ts @@ -26,8 +26,8 @@ import { assertIsDefined } from 'vs/base/common/types'; export class ToggleOutputAction extends TogglePanelAction { - public static readonly ID = 'workbench.action.output.toggleOutput'; - public static readonly LABEL = nls.localize('toggleOutput', "Toggle Output"); + static readonly ID = 'workbench.action.output.toggleOutput'; + static readonly LABEL = nls.localize('toggleOutput', "Toggle Output"); constructor( id: string, label: string, @@ -40,8 +40,8 @@ export class ToggleOutputAction extends TogglePanelAction { export class ClearOutputAction extends Action { - public static readonly ID = 'workbench.output.action.clearOutput'; - public static readonly LABEL = nls.localize('clearOutput', "Clear Output"); + static readonly ID = 'workbench.output.action.clearOutput'; + static readonly LABEL = nls.localize('clearOutput', "Clear Output"); constructor( id: string, label: string, @@ -50,7 +50,7 @@ export class ClearOutputAction extends Action { super(id, label, 'output-action codicon-clear-all'); } - public run(): Promise { + run(): Promise { const activeChannel = this.outputService.getActiveChannel(); if (activeChannel) { activeChannel.clear(); @@ -65,8 +65,8 @@ export class ClearOutputAction extends Action { // 2. user clicks inside the output panel, which sets the lock, Or unsets it if they click the last line. export class ToggleOrSetOutputScrollLockAction extends Action { - public static readonly ID = 'workbench.output.action.toggleOutputScrollLock'; - public static readonly LABEL = nls.localize({ key: 'toggleOutputScrollLock', comment: ['Turn on / off automatic output scrolling'] }, "Toggle Output Scroll Lock"); + static readonly ID = 'workbench.output.action.toggleOutputScrollLock'; + static readonly LABEL = nls.localize({ key: 'toggleOutputScrollLock', comment: ['Turn on / off automatic output scrolling'] }, "Toggle Output Scroll Lock"); constructor(id: string, label: string, @IOutputService private readonly outputService: IOutputService) { super(id, label, 'output-action codicon-unlock'); @@ -78,7 +78,7 @@ export class ToggleOrSetOutputScrollLockAction extends Action { })); } - public run(newLockState?: boolean): Promise { + run(newLockState?: boolean): Promise { const activeChannel = this.outputService.getActiveChannel(); if (activeChannel) { @@ -107,7 +107,7 @@ export class ToggleOrSetOutputScrollLockAction extends Action { export class SwitchOutputAction extends Action { - public static readonly ID = 'workbench.output.action.switchBetweenOutputs'; + static readonly ID = 'workbench.output.action.switchBetweenOutputs'; constructor(@IOutputService private readonly outputService: IOutputService) { super(SwitchOutputAction.ID, nls.localize('switchToOutput.label', "Switch to Output")); @@ -115,7 +115,7 @@ export class SwitchOutputAction extends Action { this.class = 'output-action switch-to-output'; } - public run(channelId: string): Promise { + run(channelId: string): Promise { return this.outputService.showChannel(channelId); } } @@ -179,8 +179,8 @@ export class SwitchOutputActionViewItem extends SelectActionViewItem { export class OpenLogOutputFile extends Action { - public static readonly ID = 'workbench.output.action.openLogOutputFile'; - public static readonly LABEL = nls.localize('openInLogViewer', "Open Log File"); + static readonly ID = 'workbench.output.action.openLogOutputFile'; + static readonly LABEL = nls.localize('openInLogViewer', "Open Log File"); constructor( @IOutputService private readonly outputService: IOutputService, @@ -196,9 +196,11 @@ export class OpenLogOutputFile extends Action { this.enabled = !!this.getLogFileOutputChannelDescriptor(); } - public run(): Promise { + async run(): Promise { const logFileOutputChannelDescriptor = this.getLogFileOutputChannelDescriptor(); - return logFileOutputChannelDescriptor ? this.editorService.openEditor(this.instantiationService.createInstance(LogViewerInput, logFileOutputChannelDescriptor)).then(() => null) : Promise.resolve(null); + if (logFileOutputChannelDescriptor) { + await this.editorService.openEditor(this.instantiationService.createInstance(LogViewerInput, logFileOutputChannelDescriptor)); + } } private getLogFileOutputChannelDescriptor(): IFileOutputChannelDescriptor | null { @@ -225,17 +227,14 @@ export class ShowLogsOutputChannelAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { const entries: { id: string, label: string }[] = this.outputService.getChannelDescriptors().filter(c => c.file && c.log) .map(({ id, label }) => ({ id, label })); - return this.quickInputService.pick(entries, { placeHolder: nls.localize('selectlog', "Select Log") }) - .then(entry => { - if (entry) { - return this.outputService.showChannel(entry.id); - } - return undefined; - }); + const entry = await this.quickInputService.pick(entries, { placeHolder: nls.localize('selectlog', "Select Log") }); + if (entry) { + return this.outputService.showChannel(entry.id); + } } } @@ -257,17 +256,14 @@ export class OpenOutputLogFileAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { const entries: IOutputChannelQuickPickItem[] = this.outputService.getChannelDescriptors().filter(c => c.file && c.log) .map(channel => ({ id: channel.id, label: channel.label, channel })); - return this.quickInputService.pick(entries, { placeHolder: nls.localize('selectlogFile', "Select Log file") }) - .then(entry => { - if (entry) { - assertIsDefined(entry.channel.file); - return this.editorService.openEditor(this.instantiationService.createInstance(LogViewerInput, entry.channel as IFileOutputChannelDescriptor)).then(() => undefined); - } - return undefined; - }); + const entry = await this.quickInputService.pick(entries, { placeHolder: nls.localize('selectlogFile', "Select Log file") }); + if (entry) { + assertIsDefined(entry.channel.file); + await this.editorService.openEditor(this.instantiationService.createInstance(LogViewerInput, (entry.channel as IFileOutputChannelDescriptor))); + } } } diff --git a/src/vs/workbench/contrib/output/browser/outputPanel.ts b/src/vs/workbench/contrib/output/browser/outputPanel.ts index 3c23f8f2cb..59b61c0ce9 100644 --- a/src/vs/workbench/contrib/output/browser/outputPanel.ts +++ b/src/vs/workbench/contrib/output/browser/outputPanel.ts @@ -47,15 +47,15 @@ export class OutputPanel extends AbstractTextResourceEditor { this.scopedInstantiationService = instantiationService; } - public getId(): string { + getId(): string { return OUTPUT_PANEL_ID; } - public getTitle(): string { + getTitle(): string { return nls.localize('output', "Output"); } - public getActions(): IAction[] { + getActions(): IAction[] { if (!this.actions) { this.actions = [ this.instantiationService.createInstance(SwitchOutputAction), @@ -70,7 +70,7 @@ export class OutputPanel extends AbstractTextResourceEditor { return this.actions; } - public getActionViewItem(action: Action): IActionViewItem | undefined { + getActionViewItem(action: Action): IActionViewItem | undefined { if (action.id === SwitchOutputAction.ID) { return this.instantiationService.createInstance(SwitchOutputActionViewItem, action); } @@ -109,7 +109,7 @@ export class OutputPanel extends AbstractTextResourceEditor { return channel ? nls.localize('outputPanelWithInputAriaLabel', "{0}, Output panel", channel.label) : nls.localize('outputPanelAriaLabel', "Output panel"); } - public setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { this._focus = !(options && options.preserveFocus); if (input.matches(this.input)) { return Promise.resolve(undefined); @@ -119,15 +119,14 @@ export class OutputPanel extends AbstractTextResourceEditor { // Dispose previous input (Output panel is not a workbench editor) this.input.dispose(); } - return super.setInput(input, options, token).then(() => { - if (this._focus) { - this.focus(); - } - this.revealLastLine(); - }); + await super.setInput(input, options, token); + if (this._focus) { + this.focus(); + } + this.revealLastLine(); } - public clearInput(): void { + clearInput(): void { if (this.input) { // Dispose current input (Output panel is not a workbench editor) this.input.dispose(); @@ -160,7 +159,7 @@ export class OutputPanel extends AbstractTextResourceEditor { }); } - public get instantiationService(): IInstantiationService { + get instantiationService(): IInstantiationService { return this.scopedInstantiationService; } } diff --git a/src/vs/workbench/contrib/output/browser/outputServices.ts b/src/vs/workbench/contrib/output/browser/outputServices.ts index 3904bbc2c8..0933e0e58c 100644 --- a/src/vs/workbench/contrib/output/browser/outputServices.ts +++ b/src/vs/workbench/contrib/output/browser/outputServices.ts @@ -60,7 +60,7 @@ class OutputChannel extends Disposable implements IOutputChannel { export class OutputService extends Disposable implements IOutputService, ITextModelContentProvider { - public _serviceBrand: undefined; + _serviceBrand: undefined; private channels: Map = new Map(); private activeChannelIdInStorage: string; @@ -114,24 +114,21 @@ export class OutputService extends Disposable implements IOutputService, ITextMo return null; } - showChannel(id: string, preserveFocus?: boolean): Promise { + async showChannel(id: string, preserveFocus?: boolean): Promise { const channel = this.getChannel(id); if (!channel || this.isChannelShown(channel)) { if (this._outputPanel && !preserveFocus) { this._outputPanel.focus(); } - return Promise.resolve(undefined); + return; } this.setActiveChannel(channel); - let promise: Promise; - if (this.isPanelShown()) { - promise = this.doShowChannel(channel, !!preserveFocus); - } else { + if (!this.isPanelShown()) { this.panelService.openPanel(OUTPUT_PANEL_ID); - promise = this.doShowChannel(channel, !!preserveFocus); } - return promise.then(() => this._onActiveOutputChannel.fire(id)); + await this.doShowChannel(channel, !!preserveFocus); + this._onActiveOutputChannel.fire(id); } getChannel(id: string): OutputChannel | undefined { @@ -146,13 +143,13 @@ export class OutputService extends Disposable implements IOutputService, ITextMo return this.activeChannel; } - private onDidRegisterChannel(channelId: string): void { + private async onDidRegisterChannel(channelId: string): Promise { const channel = this.createChannel(channelId); this.channels.set(channelId, channel); if (!this.activeChannel || this.activeChannelIdInStorage === channelId) { this.setActiveChannel(channel); - this.onDidPanelOpen(this.panelService.getActivePanel(), true) - .then(() => this._onActiveOutputChannel.fire(channelId)); + await this.onDidPanelOpen(this.panelService.getActivePanel(), true); + this._onActiveOutputChannel.fire(channelId); } } @@ -214,17 +211,14 @@ export class OutputService extends Disposable implements IOutputService, ITextMo return this.instantiationService.createInstance(OutputChannel, channelData); } - private doShowChannel(channel: OutputChannel, preserveFocus: boolean): Promise { + private async doShowChannel(channel: OutputChannel, preserveFocus: boolean): Promise { if (this._outputPanel) { CONTEXT_ACTIVE_LOG_OUTPUT.bindTo(this.contextKeyService).set(!!channel.outputChannelDescriptor.file && channel.outputChannelDescriptor.log); - return this._outputPanel.setInput(this.createInput(channel), EditorOptions.create({ preserveFocus }), CancellationToken.None) - .then(() => { - if (!preserveFocus && this._outputPanel) { - this._outputPanel.focus(); - } - }); + await this._outputPanel.setInput(this.createInput(channel), EditorOptions.create({ preserveFocus }), CancellationToken.None); + if (!preserveFocus && this._outputPanel) { + this._outputPanel.focus(); + } } - return Promise.resolve(undefined); } private isChannelShown(channel: IOutputChannel): boolean { diff --git a/src/vs/workbench/contrib/output/common/outputLinkComputer.ts b/src/vs/workbench/contrib/output/common/outputLinkComputer.ts index 4e1aeaa3f1..c21851e11b 100644 --- a/src/vs/workbench/contrib/output/common/outputLinkComputer.ts +++ b/src/vs/workbench/contrib/output/common/outputLinkComputer.ts @@ -23,13 +23,9 @@ export interface IResourceCreator { } export class OutputLinkComputer { - private ctx: IWorkerContext; - private patterns: Map; - - constructor(ctx: IWorkerContext, createData: ICreateData) { - this.ctx = ctx; - this.patterns = new Map(); + private patterns = new Map(); + constructor(private ctx: IWorkerContext, createData: ICreateData) { this.computePatterns(createData); } @@ -38,29 +34,33 @@ export class OutputLinkComputer { // Produce patterns for each workspace root we are configured with // This means that we will be able to detect links for paths that // contain any of the workspace roots as segments. - const workspaceFolders = createData.workspaceFolders.map(r => URI.parse(r)); - workspaceFolders.forEach(workspaceFolder => { + const workspaceFolders = createData.workspaceFolders + .sort((resourceStrA, resourceStrB) => resourceStrB.length - resourceStrA.length) // longest paths first (for https://github.com/microsoft/vscode/issues/88121) + .map(resourceStr => URI.parse(resourceStr)); + + for (const workspaceFolder of workspaceFolders) { const patterns = OutputLinkComputer.createPatterns(workspaceFolder); this.patterns.set(workspaceFolder, patterns); - }); + } } private getModel(uri: string): IMirrorModel | undefined { const models = this.ctx.getMirrorModels(); + return find(models, model => model.uri.toString() === uri); } - public computeLinks(uri: string): Promise { + computeLinks(uri: string): ILink[] { const model = this.getModel(uri); if (!model) { - return Promise.resolve([]); + return []; } const links: ILink[] = []; const lines = model.getValue().split(/\r\n|\r|\n/); // For each workspace root patterns - this.patterns.forEach((folderPatterns, folderUri) => { + for (const [folderUri, folderPatterns] of this.patterns) { const resourceCreator: IResourceCreator = { toResource: (folderRelativePath: string): URI | null => { if (typeof folderRelativePath === 'string') { @@ -74,12 +74,12 @@ export class OutputLinkComputer { for (let i = 0, len = lines.length; i < len; i++) { links.push(...OutputLinkComputer.detectLinks(lines[i], i + 1, folderPatterns, resourceCreator)); } - }); + } - return Promise.resolve(links); + return links; } - public static createPatterns(workspaceFolder: URI): RegExp[] { + static createPatterns(workspaceFolder: URI): RegExp[] { const patterns: RegExp[] = []; const workspaceFolderPath = workspaceFolder.scheme === Schemas.file ? workspaceFolder.fsPath : workspaceFolder.path; @@ -88,7 +88,7 @@ export class OutputLinkComputer { workspaceFolderVariants.push(extpath.toSlashes(workspaceFolderPath)); } - workspaceFolderVariants.forEach(workspaceFolderVariant => { + for (const workspaceFolderVariant of workspaceFolderVariants) { const validPathCharacterPattern = '[^\\s\\(\\):<>"]'; const validPathCharacterOrSpacePattern = `(?:${validPathCharacterPattern}| ${validPathCharacterPattern})`; const pathPattern = `${validPathCharacterOrSpacePattern}+\\.${validPathCharacterPattern}+`; @@ -111,15 +111,15 @@ export class OutputLinkComputer { // Example: at /workspaces/mankala/Game.ts:336 // Example: at /workspaces/mankala/Game.ts:336:9 patterns.push(new RegExp(strings.escapeRegExpCharacters(workspaceFolderVariant) + `(${strictPathPattern})(:(\\d+))?(:(\\d+))?`, 'gi')); - }); + } return patterns; } /** - * Detect links. Made public static to allow for tests. + * Detect links. Made static to allow for tests. */ - public static detectLinks(line: string, lineIndex: number, patterns: RegExp[], resourceCreator: IResourceCreator): ILink[] { + static detectLinks(line: string, lineIndex: number, patterns: RegExp[], resourceCreator: IResourceCreator): ILink[] { const links: ILink[] = []; patterns.forEach(pattern => { diff --git a/src/vs/workbench/contrib/output/common/outputLinkProvider.ts b/src/vs/workbench/contrib/output/common/outputLinkProvider.ts index 223091eccf..933b89e2dd 100644 --- a/src/vs/workbench/contrib/output/common/outputLinkProvider.ts +++ b/src/vs/workbench/contrib/output/common/outputLinkProvider.ts @@ -6,7 +6,7 @@ import { URI } from 'vs/base/common/uri'; import { RunOnceScheduler } from 'vs/base/common/async'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { LinkProviderRegistry, ILink, ILinksList } from 'vs/editor/common/modes'; +import { LinkProviderRegistry, ILink } from 'vs/editor/common/modes'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { OUTPUT_MODE_ID, LOG_MODE_ID } from 'vs/workbench/contrib/output/common/output'; import { MonacoWebWorker, createWebWorker } from 'vs/editor/common/services/webWorker'; @@ -42,8 +42,10 @@ export class OutputLinkProvider { if (folders.length > 0) { if (!this.linkProviderRegistration) { this.linkProviderRegistration = LinkProviderRegistry.register([{ language: OUTPUT_MODE_ID, scheme: '*' }, { language: LOG_MODE_ID, scheme: '*' }], { - provideLinks: (model): Promise => { - return this.provideLinks(model.uri).then(links => links && { links }); + provideLinks: async model => { + const links = await this.provideLinks(model.uri); + + return links && { links }; } }); } @@ -75,10 +77,10 @@ export class OutputLinkProvider { return this.worker; } - private provideLinks(modelUri: URI): Promise { - return this.getOrCreateWorker().withSyncedResources([modelUri]).then(linkComputer => { - return linkComputer.computeLinks(modelUri.toString()); - }); + private async provideLinks(modelUri: URI): Promise { + const linkComputer = await this.getOrCreateWorker().withSyncedResources([modelUri]); + + return linkComputer.computeLinks(modelUri.toString()); } private disposeWorker(): void { diff --git a/src/vs/workbench/contrib/output/test/outputLinkProvider.test.ts b/src/vs/workbench/contrib/output/test/outputLinkProvider.test.ts index b0f8a79192..0a1366e314 100644 --- a/src/vs/workbench/contrib/output/test/outputLinkProvider.test.ts +++ b/src/vs/workbench/contrib/output/test/outputLinkProvider.test.ts @@ -17,9 +17,9 @@ function toOSPath(p: string): string { return p; } -suite('Workbench - OutputWorker', () => { +suite('OutputLinkProvider', () => { - test('OutputWorker - Link detection', function () { + test('OutputLinkProvider - Link detection', function () { const rootFolder = isWindows ? URI.file('C:\\Users\\someone\\AppData\\Local\\Temp\\_monacodata_9888\\workspaces\\mankala') : URI.file('C:/Users/someone/AppData/Local/Temp/_monacodata_9888/workspaces/mankala'); @@ -107,7 +107,6 @@ suite('Workbench - OutputWorker', () => { assert.equal(result[0].range.startColumn, 1); assert.equal(result[0].range.endColumn, 101); - // Example: C:\Users\someone\AppData\Local\Temp\_monacodata_9888\workspaces\express\server.js:line 8 line = toOSPath('C:\\Users\\someone\\AppData\\Local\\Temp\\_monacodata_9888\\workspaces\\mankala\\Game.ts:line 8'); result = OutputLinkComputer.detectLinks(line, 1, patterns, contextService); @@ -281,4 +280,4 @@ suite('Workbench - OutputWorker', () => { assert.equal(result[0].range.startColumn, 6); assert.equal(result[0].range.endColumn, 86); }); -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/contrib/performance/electron-browser/perfviewEditor.ts b/src/vs/workbench/contrib/performance/electron-browser/perfviewEditor.ts index b5f53dd469..3f583420fe 100644 --- a/src/vs/workbench/contrib/performance/electron-browser/perfviewEditor.ts +++ b/src/vs/workbench/contrib/performance/electron-browser/perfviewEditor.ts @@ -21,6 +21,8 @@ import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService import { writeTransientState } from 'vs/workbench/contrib/codeEditor/browser/toggleWordWrap'; import { mergeSort } from 'vs/base/common/arrays'; import product from 'vs/platform/product/common/product'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export class PerfviewContrib { @@ -44,14 +46,18 @@ export class PerfviewInput extends ResourceEditorInput { static readonly Uri = URI.from({ scheme: 'perf', path: 'Startup Performance' }); constructor( - @ITextModelService textModelResolverService: ITextModelService + @ITextModelService textModelResolverService: ITextModelService, + @ITextFileService textFileService: ITextFileService, + @IEditorService editorService: IEditorService ) { super( localize('name', "Startup Performance"), undefined, PerfviewInput.Uri, undefined, - textModelResolverService + textModelResolverService, + textFileService, + editorService ); } diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts b/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts index 655ff9e194..cb984a11e9 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts @@ -188,7 +188,7 @@ export class DefineKeybindingWidget extends Widget { if (colors.editorWidgetForeground) { this._domNode.domNode.style.color = colors.editorWidgetForeground.toString(); } else { - this._domNode.domNode.style.color = null; + this._domNode.domNode.style.color = ''; } if (colors.widgetShadow) { diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index af18b1667d..885653785c 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -410,7 +410,7 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditor recordingBadge.style.borderWidth = border ? '1px' : ''; recordingBadge.style.borderStyle = border ? 'solid' : ''; recordingBadge.style.borderColor = border; - recordingBadge.style.color = color ? color.toString() : null; + recordingBadge.style.color = color ? color.toString() : ''; })); return recordingBadge; } diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditorContribution.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditorContribution.ts index 2973c2fce7..63eac5f9bb 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditorContribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditorContribution.ts @@ -12,7 +12,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { Range } from 'vs/editor/common/core/range'; -import * as editorCommon from 'vs/editor/common/editorCommon'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { registerEditorContribution, ServicesAccessor, registerEditorCommand, EditorCommand } from 'vs/editor/browser/editorExtensions'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; @@ -37,7 +37,7 @@ const NLS_KB_LAYOUT_ERROR_MESSAGE = nls.localize('defineKeybinding.kbLayoutError const INTERESTING_FILE = /keybindings\.json$/; -export class DefineKeybindingController extends Disposable implements editorCommon.IEditorContribution { +export class DefineKeybindingController extends Disposable implements IEditorContribution { public static readonly ID = 'editor.contrib.defineKeybinding'; diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts index edcd5bb032..fb423d92e7 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts @@ -643,7 +643,7 @@ export class SearchWidget extends Widget { this.countElement.style.borderColor = border; const color = this.themeService.getTheme().getColor(badgeForeground); - this.countElement.style.color = color ? color.toString() : null; + this.countElement.style.color = color ? color.toString() : ''; })); } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index d5e3bf1212..aa7b2b0079 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -541,7 +541,7 @@ export class SettingsEditor2 extends BaseEditor { DOM.append(this.noResultsMessage, this.clearFilterLinkContainer); this._register(attachStylerCallback(this.themeService, { editorForeground }, colors => { - this.noResultsMessage.style.color = colors.editorForeground ? colors.editorForeground.toString() : null; + this.noResultsMessage.style.color = colors.editorForeground ? colors.editorForeground.toString() : ''; })); this.createTOC(bodyContainer); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index d7f105b6aa..bbe0d0a361 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -3,16 +3,17 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { BrowserFeatures } from 'vs/base/browser/canIUse'; import * as DOM from 'vs/base/browser/dom'; -import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { alert as ariaAlert } from 'vs/base/browser/ui/aria/aria'; import { Button } from 'vs/base/browser/ui/button/button'; import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox'; import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; -import { ListAriaRootRole, CachedListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { CachedListVirtualDelegate, ListAriaRootRole } from 'vs/base/browser/ui/list/list'; import { DefaultStyleController } from 'vs/base/browser/ui/list/listWidget'; import { ISelectOptionItem, SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; @@ -25,29 +26,29 @@ import { Color, RGBA } from 'vs/base/common/color'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { dispose, IDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { isIOS } from 'vs/base/common/platform'; import { ISpliceable } from 'vs/base/common/sequence'; import { escapeRegExpCharacters, startsWith } from 'vs/base/common/strings'; +import { isArray } from 'vs/base/common/types'; import { localize } from 'vs/nls'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { errorForeground, focusBorder, foreground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, editorBackground } from 'vs/platform/theme/common/colorRegistry'; +import { editorBackground, errorForeground, focusBorder, foreground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground } from 'vs/platform/theme/common/colorRegistry'; import { attachButtonStyler, attachInputBoxStyler, attachSelectBoxStyler, attachStyler } from 'vs/platform/theme/common/styler'; import { ICssStyleCollector, ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { getIgnoredSettings } from 'vs/platform/userDataSync/common/settingsSync'; import { ITOCEntry } from 'vs/workbench/contrib/preferences/browser/settingsLayout'; import { ISettingsEditorViewState, settingKeyToDisplayFormat, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeNewExtensionsElement, SettingsTreeSettingElement } from 'vs/workbench/contrib/preferences/browser/settingsTreeModels'; -import { ListSettingWidget, IListChangeEvent, IListDataItem, settingsHeaderForeground, settingsNumberInputBackground, settingsNumberInputBorder, settingsNumberInputForeground, settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground, ExcludeSettingWidget } from 'vs/workbench/contrib/preferences/browser/settingsWidgets'; +import { ExcludeSettingWidget, IListChangeEvent, IListDataItem, ListSettingWidget, settingsHeaderForeground, settingsNumberInputBackground, settingsNumberInputBorder, settingsNumberInputForeground, settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground } from 'vs/workbench/contrib/preferences/browser/settingsWidgets'; import { SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU } from 'vs/workbench/contrib/preferences/common/preferences'; -import { ISetting, ISettingsGroup, SettingValueType } from 'vs/workbench/services/preferences/common/preferences'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { isArray } from 'vs/base/common/types'; -import { BrowserFeatures } from 'vs/base/browser/canIUse'; -import { isIOS } from 'vs/base/common/platform'; +import { ISetting, ISettingsGroup, SettingValueType } from 'vs/workbench/services/preferences/common/preferences'; const $ = DOM.$; @@ -203,6 +204,7 @@ interface ISettingItemTemplate extends IDisposableTemplate { deprecationWarningElement: HTMLElement; otherOverridesElement: HTMLElement; toolbar: ToolBar; + elementDisposables: IDisposable[]; } interface ISettingBoolItemTemplate extends ISettingItemTemplate { @@ -300,6 +302,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre // Put common injections back here constructor( private readonly settingActions: IAction[], + private readonly disposableActionFactory: (setting: ISetting) => IAction[], @IThemeService protected readonly _themeService: IThemeService, @IContextViewService protected readonly _contextViewService: IContextViewService, @IOpenerService protected readonly _openerService: IOpenerService, @@ -345,6 +348,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre const template: ISettingItemTemplate = { toDispose, + elementDisposables: [], containerElement: container, categoryElement, @@ -393,23 +397,27 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre const toolbar = new ToolBar(container, this._contextMenuService, { toggleMenuTitle }); - toolbar.setActions([], this.settingActions)(); + return toolbar; + } - const button = container.querySelector('.codicon-more'); + private fixToolbarIcon(toolbar: ToolBar): void { + const button = toolbar.getContainer().querySelector('.codicon-more'); if (button) { (button).tabIndex = -1; // change icon from ellipsis to gear (button).classList.add('codicon-gear'); } - - return toolbar; } protected renderSettingElement(node: ITreeNode, index: number, template: ISettingItemTemplate | ISettingBoolItemTemplate): void { const element = node.element; template.context = element; template.toolbar.context = element; + const actions = this.disposableActionFactory(element.setting); + template.elementDisposables?.push(...actions); + template.toolbar.setActions([], [...this.settingActions, ...actions])(); + this.fixToolbarIcon(template.toolbar); const setting = element.setting; @@ -455,14 +463,15 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre DOM.append(template.otherOverridesElement, $('span', undefined, ')')); } - DOM.addStandardDisposableListener(view, DOM.EventType.CLICK, (e: IMouseEvent) => { - this._onDidClickOverrideElement.fire({ - targetKey: element.setting.key, - scope: element.overriddenScopeList[i] - }); - e.preventDefault(); - e.stopPropagation(); - }); + template.elementDisposables.push( + DOM.addStandardDisposableListener(view, DOM.EventType.CLICK, (e: IMouseEvent) => { + this._onDidClickOverrideElement.fire({ + targetKey: element.setting.key, + scope: element.overriddenScopeList[i] + }); + e.preventDefault(); + e.stopPropagation(); + })); } } @@ -472,7 +481,6 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre DOM.toggleClass(template.containerElement, 'is-deprecated', !!deprecationText); this.renderValue(element, template, onChange); - } private renderDescriptionMarkdown(element: SettingsTreeSettingElement, text: string, disposeables: DisposableStore): HTMLElement { @@ -563,6 +571,12 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre disposeTemplate(template: IDisposableTemplate): void { dispose(template.toDispose); } + + disposeElement(_element: ITreeNode, _index: number, template: IDisposableTemplate, _height: number | undefined): void { + if ((template as ISettingItemTemplate).elementDisposables) { + dispose((template as ISettingItemTemplate).elementDisposables); + } + } } export class SettingGroupRenderer implements ITreeRenderer { @@ -1113,6 +1127,7 @@ export class SettingBoolRenderer extends AbstractSettingRenderer implements ITre const template: ISettingBoolItemTemplate = { toDispose: [toDispose], + elementDisposables: [], containerElement: container, categoryElement, @@ -1190,15 +1205,16 @@ export class SettingTreeRenderers { this._instantiationService.createInstance(CopySettingAsJSONAction), ]; + const actionFactory = (setting: ISetting) => [this._instantiationService.createInstance(StopSyncingSettingAction, setting)]; const settingRenderers = [ - this._instantiationService.createInstance(SettingBoolRenderer, this.settingActions), - this._instantiationService.createInstance(SettingNumberRenderer, this.settingActions), - this._instantiationService.createInstance(SettingBoolRenderer, this.settingActions), - this._instantiationService.createInstance(SettingArrayRenderer, this.settingActions), - this._instantiationService.createInstance(SettingComplexRenderer, this.settingActions), - this._instantiationService.createInstance(SettingTextRenderer, this.settingActions), - this._instantiationService.createInstance(SettingExcludeRenderer, this.settingActions), - this._instantiationService.createInstance(SettingEnumRenderer, this.settingActions), + this._instantiationService.createInstance(SettingBoolRenderer, this.settingActions, actionFactory), + this._instantiationService.createInstance(SettingNumberRenderer, this.settingActions, actionFactory), + this._instantiationService.createInstance(SettingBoolRenderer, this.settingActions, actionFactory), + this._instantiationService.createInstance(SettingArrayRenderer, this.settingActions, actionFactory), + this._instantiationService.createInstance(SettingComplexRenderer, this.settingActions, actionFactory), + this._instantiationService.createInstance(SettingTextRenderer, this.settingActions, actionFactory), + this._instantiationService.createInstance(SettingExcludeRenderer, this.settingActions, actionFactory), + this._instantiationService.createInstance(SettingEnumRenderer, this.settingActions, actionFactory), ]; this.onDidClickOverrideElement = Event.any(...settingRenderers.map(r => r.onDidClickOverrideElement)); @@ -1596,3 +1612,32 @@ class CopySettingAsJSONAction extends Action { return Promise.resolve(undefined); } } + +class StopSyncingSettingAction extends Action { + static readonly ID = 'settings.stopSyncingSetting'; + static readonly LABEL = localize('stopSyncingSetting', "Don't Sync This Setting"); + + constructor( + private readonly setting: ISetting, + @IConfigurationService private readonly configService: IConfigurationService, + ) { + super(StopSyncingSettingAction.ID, StopSyncingSettingAction.LABEL); + this.update(); + } + + update() { + const ignoredSettings = getIgnoredSettings(this.configService); + this.checked = ignoredSettings.includes(this.setting.key); + } + + async run(): Promise { + const currentValue = this.configService.getValue('sync.ignoredSettings'); + if (this.checked) { + this.configService.updateValue('sync.ignoredSettings', currentValue.filter(v => v !== this.setting.key)); + } else { + this.configService.updateValue('sync.ignoredSettings', [...currentValue, this.setting.key]); + } + + return Promise.resolve(undefined); + } +} diff --git a/src/vs/workbench/contrib/quickopen/browser/viewPickerHandler.ts b/src/vs/workbench/contrib/quickopen/browser/viewPickerHandler.ts index dbafc0e718..f7a12a44ae 100644 --- a/src/vs/workbench/contrib/quickopen/browser/viewPickerHandler.ts +++ b/src/vs/workbench/contrib/quickopen/browser/viewPickerHandler.ts @@ -16,7 +16,7 @@ import { Action } from 'vs/base/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { fuzzyContains, stripWildcards } from 'vs/base/common/strings'; import { matchesFuzzy } from 'vs/base/common/filters'; -import { IViewsRegistry, ViewContainer, IViewsService, IViewContainersRegistry, Extensions as ViewExtensions } from 'vs/workbench/common/views'; +import { IViewsRegistry, ViewContainer, IViewDescriptorService, IViewContainersRegistry, Extensions as ViewExtensions, IViewsService } from 'vs/workbench/common/views'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ViewletDescriptor } from 'vs/workbench/browser/viewlet'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -70,6 +70,7 @@ export class ViewPickerHandler extends QuickOpenHandler { constructor( @IViewletService private readonly viewletService: IViewletService, + @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, @IViewsService private readonly viewsService: IViewsService, @IOutputService private readonly outputService: IOutputService, @ITerminalService private readonly terminalService: ITerminalService, @@ -197,8 +198,8 @@ export class ViewPickerHandler extends QuickOpenHandler { private hasToShowViewlet(viewlet: ViewletDescriptor): boolean { const viewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).get(viewlet.id); if (viewContainer?.hideIfEmpty) { - const viewsCollection = this.viewsService.getViewDescriptors(viewContainer); - return !!viewsCollection && viewsCollection.activeViewDescriptors.length > 0; + const viewsCollection = this.viewDescriptorService.getViewDescriptors(viewContainer); + return viewsCollection.activeViewDescriptors.length > 0; } return true; } diff --git a/src/vs/workbench/contrib/remote/browser/explorerViewItems.ts b/src/vs/workbench/contrib/remote/browser/explorerViewItems.ts index 05e70c035e..7361a45b0a 100644 --- a/src/vs/workbench/contrib/remote/browser/explorerViewItems.ts +++ b/src/vs/workbench/contrib/remote/browser/explorerViewItems.ts @@ -51,23 +51,25 @@ export class SwitchRemoteViewItem extends SelectActionViewItem { if (this.optionsItems.length > 0) { let index = 0; const remoteAuthority = environmentService.configuration.remoteAuthority; - const explorerType: string | undefined = remoteAuthority ? remoteAuthority.split('+')[0] : - this.storageService.get(REMOTE_EXPLORER_TYPE_KEY, StorageScope.WORKSPACE) ?? this.storageService.get(REMOTE_EXPLORER_TYPE_KEY, StorageScope.GLOBAL); - if (explorerType) { + const explorerType: string[] | undefined = remoteAuthority ? [remoteAuthority.split('+')[0]] : + this.storageService.get(REMOTE_EXPLORER_TYPE_KEY, StorageScope.WORKSPACE)?.split(',') ?? this.storageService.get(REMOTE_EXPLORER_TYPE_KEY, StorageScope.GLOBAL)?.split(','); + if (explorerType !== undefined) { index = this.getOptionIndexForExplorerType(optionsItems, explorerType); } this.select(index); - remoteExplorerService.targetType = optionsItems[index].authority[0]; + remoteExplorerService.targetType = optionsItems[index].authority; } } - private getOptionIndexForExplorerType(optionsItems: IRemoteSelectItem[], explorerType: string): number { + private getOptionIndexForExplorerType(optionsItems: IRemoteSelectItem[], explorerType: string[]): number { let index = 0; for (let optionIterator = 0; (optionIterator < this.optionsItems.length) && (index === 0); optionIterator++) { for (let authorityIterator = 0; authorityIterator < optionsItems[optionIterator].authority.length; authorityIterator++) { - if (optionsItems[optionIterator].authority[authorityIterator] === explorerType) { - index = optionIterator; - break; + for (let i = 0; i < explorerType.length; i++) { + if (optionsItems[optionIterator].authority[authorityIterator] === explorerType[i]) { + index = optionIterator; + break; + } } } } @@ -110,6 +112,6 @@ export class SwitchRemoteAction extends Action { } public async run(item: IRemoteSelectItem): Promise { - this.remoteExplorerService.targetType = item.authority[0]; + this.remoteExplorerService.targetType = item.authority; } } diff --git a/src/vs/workbench/contrib/remote/browser/remote.ts b/src/vs/workbench/contrib/remote/browser/remote.ts index 012c3275f2..24d395feab 100644 --- a/src/vs/workbench/contrib/remote/browser/remote.ts +++ b/src/vs/workbench/contrib/remote/browser/remote.ts @@ -54,6 +54,7 @@ import { WorkbenchAsyncDataTree, TreeResourceNavigator2 } from 'vs/platform/list import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { Event } from 'vs/base/common/event'; import { ExtensionsRegistry, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; export interface HelpInformation { extensionDescription: IExtensionDescription; @@ -277,13 +278,18 @@ abstract class HelpItemBase implements IHelpItem { async handleClick() { const remoteAuthority = this.environmentService.configuration.remoteAuthority; - if (remoteAuthority && startsWith(remoteAuthority, this.remoteExplorerService.targetType)) { - for (let value of this.values) { - if (value.remoteAuthority) { - for (let authority of value.remoteAuthority) { - if (startsWith(remoteAuthority, authority)) { - await this.takeAction(value.extensionDescription, value.url); - return; + if (!remoteAuthority) { + return; + } + for (let i = 0; i < this.remoteExplorerService.targetType.length; i++) { + if (startsWith(remoteAuthority, this.remoteExplorerService.targetType[i])) { + for (let value of this.values) { + if (value.remoteAuthority) { + for (let authority of value.remoteAuthority) { + if (startsWith(remoteAuthority, authority)) { + await this.takeAction(value.extensionDescription, value.url); + return; + } } } } @@ -367,7 +373,7 @@ class HelpPanel extends ViewPane { @IRemoteExplorerService protected readonly remoteExplorerService: IRemoteExplorerService, @IWorkbenchEnvironmentService protected readonly workbenchEnvironmentService: IWorkbenchEnvironmentService ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, instantiationService); } protected renderBody(container: HTMLElement): void { @@ -406,13 +412,14 @@ class HelpPanel extends ViewPane { class HelpPanelDescriptor implements IViewDescriptor { readonly id = HelpPanel.ID; readonly name = HelpPanel.TITLE; - readonly ctorDescriptor: { ctor: any, arguments?: any[] }; + readonly ctorDescriptor: SyncDescriptor; readonly canToggleVisibility = true; readonly hideByDefault = false; readonly workspace = true; + readonly group = 'help@50'; constructor(viewModel: IViewModel) { - this.ctorDescriptor = { ctor: HelpPanel, arguments: [viewModel] }; + this.ctorDescriptor = new SyncDescriptor(HelpPanel, [viewModel]); } } @@ -521,7 +528,7 @@ Registry.as(Extensions.ViewContainersRegistry).register { id: VIEWLET_ID, name: nls.localize('remote.explorer', "Remote Explorer"), - ctorDescriptor: { ctor: RemoteViewPaneContainer }, + ctorDescriptor: new SyncDescriptor(RemoteViewPaneContainer), hideIfEmpty: true, viewOrderDelegate: { getOrder: (group?: string) => { @@ -540,6 +547,11 @@ Registry.as(Extensions.ViewContainersRegistry).register return -500; } + matches = /^help(@(\d+))?$/.exec(group); + if (matches) { + return -10; + } + return undefined; // {{SQL CARBON EDIT}} strict-null-check; } }, diff --git a/src/vs/workbench/contrib/remote/browser/remoteViewlet.css b/src/vs/workbench/contrib/remote/browser/remoteViewlet.css index b4eef10c91..b2a341382d 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteViewlet.css +++ b/src/vs/workbench/contrib/remote/browser/remoteViewlet.css @@ -29,7 +29,7 @@ width: 0px !important; } -.remote-help-tree-node-item-icon .monaco-icon-label-description-container { +.remote-help-tree-node-item-icon .monaco-icon-label-container > .monaco-icon-name-container { padding-left: 22px; } diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 1fb621dc14..0adc74737c 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -18,7 +18,7 @@ import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { ICommandService, ICommandHandler, CommandsRegistry } from 'vs/platform/commands/common/commands'; import { Event, Emitter } from 'vs/base/common/event'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { ITreeRenderer, ITreeNode, IAsyncDataSource, ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; +import { ITreeRenderer, ITreeNode, IAsyncDataSource, ITreeContextMenuEvent, ITreeMouseEvent } from 'vs/base/browser/ui/tree/tree'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { Disposable, IDisposable, toDisposable, MutableDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; import { ActionBar, ActionViewItem, IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -37,6 +37,8 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { URI } from 'vs/base/common/uri'; +import { RemoteTunnel } from 'vs/platform/remote/common/tunnel'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; export const forwardedPortsViewEnabled = new RawContextKey('forwardedPortsViewEnabled', false); @@ -55,7 +57,7 @@ export interface ITunnelViewModel { readonly forwarded: TunnelItem[]; readonly detected: TunnelItem[]; readonly candidates: Promise; - readonly input: ITunnelItem | ITunnelGroup | undefined; + readonly input: TunnelItem; groups(): Promise; } @@ -63,16 +65,23 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { private _onForwardedPortsChanged: Emitter = new Emitter(); public onForwardedPortsChanged: Event = this._onForwardedPortsChanged.event; private model: TunnelModel; - private _input: ITunnelItem | ITunnelGroup | undefined; + private _input: TunnelItem; constructor( - @IRemoteExplorerService remoteExplorerService: IRemoteExplorerService) { + @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService) { super(); this.model = remoteExplorerService.tunnelModel; this._register(this.model.onForwardPort(() => this._onForwardedPortsChanged.fire())); this._register(this.model.onClosePort(() => this._onForwardedPortsChanged.fire())); this._register(this.model.onPortName(() => this._onForwardedPortsChanged.fire())); this._register(this.model.onCandidatesChanged(() => this._onForwardedPortsChanged.fire())); + this._input = { + label: nls.localize('remote.tunnelsView.add', "Forward a Port..."), + tunnelType: TunnelType.Add, + remoteHost: 'localhost', + remotePort: 0, + description: '' + }; } async groups(): Promise { @@ -86,7 +95,7 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { } if (this.model.detected.size > 0) { groups.push({ - label: nls.localize('remote.tunnelsView.detected', "Detected"), + label: nls.localize('remote.tunnelsView.detected', "Existing Tunnels"), tunnelType: TunnelType.Detected, items: this.detected }); @@ -94,25 +103,25 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { const candidates = await this.candidates; if (candidates.length > 0) { groups.push({ - label: nls.localize('remote.tunnelsView.candidates', "Candidates"), + label: nls.localize('remote.tunnelsView.candidates', "Not Forwarded"), tunnelType: TunnelType.Candidate, items: candidates }); } - if (!this._input) { - this._input = { - label: nls.localize('remote.tunnelsView.add', "Forward a Port..."), - tunnelType: TunnelType.Add, - }; + if (groups.length === 0) { + groups.push(this._input); } - groups.push(this._input); return groups; } get forwarded(): TunnelItem[] { - return Array.from(this.model.forwarded.values()).map(tunnel => { + const forwarded = Array.from(this.model.forwarded.values()).map(tunnel => { return new TunnelItem(TunnelType.Forwarded, tunnel.remoteHost, tunnel.remotePort, tunnel.localAddress, tunnel.closeable, tunnel.name, tunnel.description); }); + if (this.remoteExplorerService.getEditableData(undefined)) { + forwarded.push(this._input); + } + return forwarded; } get detected(): TunnelItem[] { @@ -134,7 +143,7 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { }); } - get input(): ITunnelItem | ITunnelGroup | undefined { + get input(): TunnelItem { return this._input; } @@ -225,7 +234,8 @@ class TunnelTreeRenderer extends Disposable implements ITreeRenderer this.onContextMenu(e, actionRunner))); + this._register(this.tree.onMouseDblClick(e => this.onMouseDblClick(e))); this.tree.setInput(this.viewModel); this._register(this.viewModel.onForwardedPortsChanged(() => { @@ -501,18 +516,23 @@ export class TunnelPanel extends ViewPane { } private onContextMenu(treeEvent: ITreeContextMenuEvent, actionRunner: ActionRunner): void { - if (!(treeEvent.element instanceof TunnelItem)) { + if ((treeEvent.element !== null) && !(treeEvent.element instanceof TunnelItem)) { return; } - const node: ITunnelItem | null = treeEvent.element; + const node: ITunnelItem | null = treeEvent.element as ITunnelItem | null; // {{SQL CARBON EDIT}} strict-null-check const event: UIEvent = treeEvent.browserEvent; event.preventDefault(); event.stopPropagation(); - this.tree!.setFocus([node]); - this.tunnelTypeContext.set(node.tunnelType); - this.tunnelCloseableContext.set(!!node.closeable); + if (node) { + this.tree!.setFocus([node]); + this.tunnelTypeContext.set(node.tunnelType); + this.tunnelCloseableContext.set(!!node.closeable); + } else { + this.tunnelTypeContext.set(TunnelType.Add); + this.tunnelCloseableContext.set(false); + } const actions: IAction[] = []; this._register(createAndFillInContextMenuActions(this.contributedContextMenu, { shouldForwardArgs: true }, actions, this.contextMenuService)); @@ -537,6 +557,12 @@ export class TunnelPanel extends ViewPane { }); } + private onMouseDblClick(e: ITreeMouseEvent): void { + if (!e.element) { + this.commandService.executeCommand(ForwardPortAction.INLINE_ID); + } + } + protected layoutBody(height: number, width: number): void { this.tree.layout(height, width); } @@ -549,7 +575,7 @@ export class TunnelPanel extends ViewPane { export class TunnelPanelDescriptor implements IViewDescriptor { readonly id = TunnelPanel.ID; readonly name = TunnelPanel.TITLE; - readonly ctorDescriptor: { ctor: any, arguments?: any[] }; + readonly ctorDescriptor: SyncDescriptor; readonly canToggleVisibility = true; readonly hideByDefault = false; readonly workspace = true; @@ -557,7 +583,7 @@ export class TunnelPanelDescriptor implements IViewDescriptor { readonly remoteAuthority?: string | string[]; constructor(viewModel: ITunnelViewModel, environmentService: IWorkbenchEnvironmentService) { - this.ctorDescriptor = { ctor: TunnelPanel, arguments: [viewModel] }; + this.ctorDescriptor = new SyncDescriptor(TunnelPanel, [viewModel]); this.remoteAuthority = environmentService.configuration.remoteAuthority ? environmentService.configuration.remoteAuthority.split('+')[0] : undefined; } } @@ -608,17 +634,24 @@ namespace ForwardPortAction { return null; } + function error(notificationService: INotificationService, tunnel: RemoteTunnel | void, host: string, port: number) { + if (!tunnel) { + notificationService.error(nls.localize('remote.tunnel.forwardError', "Unable to forward {0}:{1}. The host may not be available.", host, port)); + } + } + export function inlineHandler(): ICommandHandler { return async (accessor, arg) => { const remoteExplorerService = accessor.get(IRemoteExplorerService); + const notificationService = accessor.get(INotificationService); if (arg instanceof TunnelItem) { - remoteExplorerService.forward({ host: arg.remoteHost, port: arg.remotePort }); + remoteExplorerService.forward({ host: arg.remoteHost, port: arg.remotePort }).then(tunnel => error(notificationService, tunnel, arg.remoteHost, arg.remotePort)); } else { remoteExplorerService.setEditable(undefined, { onFinish: (value, success) => { let parsed: { host: string, port: number } | undefined; if (success && (parsed = parseInput(value))) { - remoteExplorerService.forward({ host: parsed.host, port: parsed.port }); + remoteExplorerService.forward({ host: parsed.host, port: parsed.port }).then(tunnel => error(notificationService, tunnel, parsed!.host, parsed!.port)); } remoteExplorerService.setEditable(undefined, null); }, @@ -632,6 +665,7 @@ namespace ForwardPortAction { export function commandPaletteHandler(): ICommandHandler { return async (accessor, arg) => { const remoteExplorerService = accessor.get(IRemoteExplorerService); + const notificationService = accessor.get(INotificationService); const viewsService = accessor.get(IViewsService); const quickInputService = accessor.get(IQuickInputService); await viewsService.openView(TunnelPanel.ID, true); @@ -641,7 +675,7 @@ namespace ForwardPortAction { }); let parsed: { host: string, port: number } | undefined; if (value && (parsed = parseInput(value))) { - remoteExplorerService.forward({ host: parsed.host, port: parsed.port }); + remoteExplorerService.forward({ host: parsed.host, port: parsed.port }).then(tunnel => error(notificationService, tunnel, parsed!.host, parsed!.port)); } }; } @@ -779,7 +813,7 @@ MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({ id: ForwardPortAction.INLINE_ID, title: ForwardPortAction.LABEL, }, - when: TunnelTypeContextKey.isEqualTo(TunnelType.Candidate) + when: ContextKeyExpr.or(TunnelTypeContextKey.isEqualTo(TunnelType.Candidate), TunnelTypeContextKey.isEqualTo(TunnelType.Add)) })); MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({ group: '0_manage', diff --git a/src/vs/workbench/contrib/remote/common/remote.contribution.ts b/src/vs/workbench/contrib/remote/common/remote.contribution.ts index 86d8721e33..7044512535 100644 --- a/src/vs/workbench/contrib/remote/common/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/common/remote.contribution.ts @@ -16,6 +16,7 @@ import { IOutputChannelRegistry, Extensions as OutputExt, } from 'vs/workbench/s import { localize } from 'vs/nls'; import { joinPath } from 'vs/base/common/resources'; import { Disposable } from 'vs/base/common/lifecycle'; +import { TunnelFactoryContribution } from 'vs/workbench/contrib/remote/common/tunnelFactory'; export const VIEWLET_ID = 'workbench.view.remote'; @@ -83,3 +84,4 @@ const workbenchContributionsRegistry = Registry.as | undefined => { + const tunnelPromise = workbenchEnvironmentService.options!.tunnelFactory!(tunnelOptions); + if (!tunnelPromise) { + return undefined; + } + return new Promise(resolve => { + tunnelPromise.then(tunnel => { + const remoteTunnel: RemoteTunnel = { + tunnelRemotePort: tunnel.remoteAddress.port, + tunnelRemoteHost: tunnel.remoteAddress.host, + localAddress: tunnel.localAddress, + dispose: tunnel.dispose + }; + resolve(remoteTunnel); + }); + }); + } + })); + } + } +} diff --git a/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts b/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts index 79bd50725c..afa2b30851 100644 --- a/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts @@ -12,7 +12,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { isMacintosh } from 'vs/base/common/platform'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { MenuId, IMenuService, MenuItemAction, IMenu, MenuRegistry, registerAction } from 'vs/platform/actions/common/actions'; +import { MenuId, IMenuService, MenuItemAction, IMenu, MenuRegistry, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchContributionsExtensions } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor, IStatusbarEntry } from 'vs/workbench/services/statusbar/common/statusbar'; @@ -70,29 +70,33 @@ export class RemoteWindowActiveIndicator extends Disposable implements IWorkbenc this._register(this.windowCommandMenu); const category = nls.localize('remote.category', "Remote"); - - registerAction({ - id: WINDOW_ACTIONS_COMMAND_ID, - category, - title: { value: nls.localize('remote.showMenu', "Show Remote Menu"), original: 'Show Remote Menu' }, - menu: { - menuId: MenuId.CommandPalette - }, - handler: (_accessor) => this.showIndicatorActions(this.windowCommandMenu) + const that = this; + registerAction2(class extends Action2 { + constructor() { + super({ + id: WINDOW_ACTIONS_COMMAND_ID, + category, + title: { value: nls.localize('remote.showMenu', "Show Remote Menu"), original: 'Show Remote Menu' }, + f1: true, + }); + } + run = () => that.showIndicatorActions(that.windowCommandMenu); }); this.remoteAuthority = environmentService.configuration.remoteAuthority; Deprecated_RemoteAuthorityContext.bindTo(this.contextKeyService).set(this.remoteAuthority || ''); if (this.remoteAuthority) { - registerAction({ - id: CLOSE_REMOTE_COMMAND_ID, - category, - title: { value: nls.localize('remote.close', "Close Remote Connection"), original: 'Close Remote Connection' }, - menu: { - menuId: MenuId.CommandPalette - }, - handler: (_accessor) => this.remoteAuthority && hostService.openWindow({ forceReuseWindow: true }) + registerAction2(class extends Action2 { + constructor() { + super({ + id: CLOSE_REMOTE_COMMAND_ID, + category, + title: { value: nls.localize('remote.close', "Close Remote Connection"), original: 'Close Remote Connection' }, + f1: true + }); + } + run = () => that.remoteAuthority && hostService.openWindow({ forceReuseWindow: true }); }); // Pending entry until extensions are ready diff --git a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index bcc8bea530..661560732b 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -1123,6 +1123,8 @@ export class DirtyDiffModel extends Disposable { this.originalModelDisposables.add(ref.object.textEditorModel.onDidChangeContent(() => this.triggerDiff())); return originalUri; + }).catch(error => { + return null; // possibly invalid reference }); }); diff --git a/src/vs/workbench/contrib/scm/browser/mainPane.ts b/src/vs/workbench/contrib/scm/browser/mainPane.ts index dc3d1bbe13..1bdd66f2a5 100644 --- a/src/vs/workbench/contrib/scm/browser/mainPane.ts +++ b/src/vs/workbench/contrib/scm/browser/mainPane.ts @@ -31,6 +31,7 @@ import { WorkbenchList } from 'vs/platform/list/browser/listService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IViewDescriptor } from 'vs/workbench/common/views'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; export interface ISpliceEvent { index: number; @@ -182,12 +183,12 @@ export class MainPane extends ViewPane { @IKeybindingService protected keybindingService: IKeybindingService, @IContextMenuService protected contextMenuService: IContextMenuService, @ISCMService protected scmService: ISCMService, - @IInstantiationService private readonly instantiationService: IInstantiationService, + @IInstantiationService instantiationService: IInstantiationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IMenuService private readonly menuService: IMenuService, @IConfigurationService configurationService: IConfigurationService ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, instantiationService); } protected renderBody(container: HTMLElement): void { @@ -320,7 +321,7 @@ export class MainPaneDescriptor implements IViewDescriptor { readonly id = MainPane.ID; readonly name = MainPane.TITLE; - readonly ctorDescriptor: { ctor: any, arguments?: any[] }; + readonly ctorDescriptor: SyncDescriptor; readonly canToggleVisibility = true; readonly hideByDefault = false; readonly order = -1000; @@ -328,6 +329,6 @@ export class MainPaneDescriptor implements IViewDescriptor { readonly when = ContextKeyExpr.or(ContextKeyExpr.equals('config.scm.alwaysShowProviders', true), ContextKeyExpr.and(ContextKeyExpr.notEquals('scm.providerCount', 0), ContextKeyExpr.notEquals('scm.providerCount', 1))); constructor(viewModel: IViewModel) { - this.ctorDescriptor = { ctor: MainPane, arguments: [viewModel] }; + this.ctorDescriptor = new SyncDescriptor(MainPane, [viewModel]); } } diff --git a/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css b/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css index 8165c100b7..3cc6cdd009 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css +++ b/src/vs/workbench/contrib/scm/browser/media/scmViewlet.css @@ -114,7 +114,7 @@ overflow: hidden; } -.scm-viewlet .monaco-list-row .resource > .name.strike-through > .monaco-icon-label > .monaco-icon-label-description-container > .label-name { +.scm-viewlet .monaco-list-row .resource > .name.strike-through > .monaco-icon-label > .monaco-icon-label-container > .monaco-icon-name-container > .label-name { text-decoration: line-through; } diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPane.ts b/src/vs/workbench/contrib/scm/browser/repositoryPane.ts index 8b6c7b5a7a..e270039d8f 100644 --- a/src/vs/workbench/contrib/scm/browser/repositoryPane.ts +++ b/src/vs/workbench/contrib/scm/browser/repositoryPane.ts @@ -54,6 +54,7 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag import { toResource, SideBySideEditor } from 'vs/workbench/common/editor'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { Hasher } from 'vs/base/common/hash'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; type TreeElement = ISCMResourceGroup | IResourceNode | ISCMResource; @@ -617,7 +618,7 @@ export class RepositoryPane extends ViewPane { @IMenuService protected menuService: IMenuService, @IStorageService private storageService: IStorageService ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, instantiationService); this.menus = instantiationService.createInstance(SCMMenus, this.repository.provider); this._register(this.menus); @@ -957,7 +958,7 @@ export class RepositoryViewDescriptor implements IViewDescriptor { readonly id: string; readonly name: string; - readonly ctorDescriptor: { ctor: any, arguments?: any[] }; + readonly ctorDescriptor: SyncDescriptor; readonly canToggleVisibility = true; readonly order = -500; readonly workspace = true; @@ -970,6 +971,6 @@ export class RepositoryViewDescriptor implements IViewDescriptor { this.id = `scm:repository:${hasher.value}`; this.name = repository.provider.rootUri ? basename(repository.provider.rootUri) : repository.provider.label; - this.ctorDescriptor = { ctor: RepositoryPane, arguments: [repository] }; + this.ctorDescriptor = new SyncDescriptor(RepositoryPane, [repository]); } } diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index bf2440ba50..afe7f89554 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -25,6 +25,7 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { SCMService } from 'vs/workbench/contrib/scm/common/scmService'; import { IViewContainersRegistry, ViewContainerLocation, Extensions as ViewContainerExtensions } from 'vs/workbench/common/views'; import { SCMViewPaneContainer } from 'vs/workbench/contrib/scm/browser/scmViewlet'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; class OpenSCMViewletAction extends ShowViewletAction { @@ -42,7 +43,7 @@ Registry.as(WorkbenchExtensions.Workbench) Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ id: VIEWLET_ID, name: localize('source control', "Source Control"), - ctorDescriptor: { ctor: SCMViewPaneContainer }, + ctorDescriptor: new SyncDescriptor(SCMViewPaneContainer), icon: 'codicon-source-control', order: 12 // {{SQL CARBON EDIT}} }, ViewContainerLocation.Sidebar); diff --git a/src/vs/workbench/contrib/search/browser/media/searchEditor.css b/src/vs/workbench/contrib/search/browser/media/searchEditor.css new file mode 100644 index 0000000000..2896e24089 --- /dev/null +++ b/src/vs/workbench/contrib/search/browser/media/searchEditor.css @@ -0,0 +1,165 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.search-editor { + display: flex; + flex-direction: column; +} + +.search-editor .search-results { + flex: 1; +} + +.search-editor .query-container { + margin: 0px 12px 12px 2px; + padding-top: 6px; +} + +.search-editor .search-widget .toggle-replace-button { + position: absolute; + top: 0; + left: 0; + width: 16px; + height: 100%; + box-sizing: border-box; + background-position: center center; + background-repeat: no-repeat; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} + +.search-editor .search-widget .search-container, +.search-editor .search-widget .replace-container { + margin-left: 18px; + display: flex; + align-items: center; +} + +.search-editor .search-widget .monaco-findInput { + display: inline-block; + vertical-align: middle; + width: 100%; +} + +.search-editor .search-widget .monaco-inputbox > .wrapper { + height: 100%; +} + +.search-editor .search-widget .monaco-inputbox > .wrapper > .mirror, +.search-editor .search-widget .monaco-inputbox > .wrapper > textarea.input { + padding: 3px; + padding-left: 4px; +} + +.search-editor .search-widget .monaco-inputbox > .wrapper > .mirror { + max-height: 134px; +} + +/* NOTE: height is also used in searchWidget.ts as a constant*/ +.search-editor .search-widget .monaco-inputbox > .wrapper > textarea.input { + overflow: initial; + height: 24px; /* set initial height before measure */ +} + +.search-editor .monaco-inputbox > .wrapper > textarea.input { + scrollbar-width: none; /* Firefox: hide scrollbar */ +} + +.search-editor .monaco-inputbox > .wrapper > textarea.input::-webkit-scrollbar { + display: none; +} + +.search-editor .search-widget .context-lines-input { + display: none; +} + +.search-editor .search-widget.show-context .context-lines-input { + display: inherit; + margin-left: 5px; + margin-right: 2px; + max-width: 50px; +} + + +.search-editor .search-widget .replace-container { + margin-top: 6px; + position: relative; + display: inline-flex; +} + +.search-editor .search-widget .replace-input { + position: relative; + display: flex; + vertical-align: middle; + width: auto !important; + height: 25px; +} + +.search-editor .search-widget .replace-input > .controls { + position: absolute; + top: 3px; + right: 2px; +} + +.search-editor .search-widget .replace-container.disabled { + display: none; +} + +.search-editor .search-widget .replace-container .monaco-action-bar { + margin-left: 0; +} + +.search-editor .search-widget .replace-container .monaco-action-bar { + height: 25px; +} + +.search-editor .search-widget .replace-container .monaco-action-bar .action-item .codicon { + background-repeat: no-repeat; + width: 25px; + height: 25px; + margin-right: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.search-editor .includes-excludes { + min-height: 1em; + position: relative; + margin: 0 0 0 17px; +} + +.search-editor .includes-excludes .expand { + position: absolute; + right: -2px; + cursor: pointer; + width: 25px; + height: 16px; + z-index: 2; /* Force it above the search results message, which has a negative top margin */ +} + +.search-editor .includes-excludes .file-types { + display: none; +} + +.search-editor .includes-excludes.expanded .file-types { + display: inherit; +} + +.search-editor .includes-excludes.expanded .file-types:last-child { + padding-bottom: 10px; +} + +.search-editor .includes-excludes.expanded h4 { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + padding: 4px 0 0; + margin: 0; + font-size: 11px; + font-weight: normal; +} diff --git a/src/vs/workbench/contrib/search/browser/openFileHandler.ts b/src/vs/workbench/contrib/search/browser/openFileHandler.ts index 5cc388272b..e2ca036d62 100644 --- a/src/vs/workbench/contrib/search/browser/openFileHandler.ts +++ b/src/vs/workbench/contrib/search/browser/openFileHandler.ts @@ -21,7 +21,6 @@ import { IModeService } from 'vs/editor/common/services/modeService'; import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IResourceInput } from 'vs/platform/editor/common/editor'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; @@ -121,11 +120,10 @@ export class OpenFileHandler extends QuickOpenHandler { @IWorkbenchThemeService private readonly themeService: IWorkbenchThemeService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @ISearchService private readonly searchService: ISearchService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IRemotePathService private readonly remotePathService: IRemotePathService, @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, @IFileService private readonly fileService: IFileService, - @ILabelService private readonly labelService: ILabelService, - @IRemotePathService private readonly remotePathService: IRemotePathService, + @ILabelService private readonly labelService: ILabelService ) { super(); @@ -187,11 +185,12 @@ export class OpenFileHandler extends QuickOpenHandler { } private async getAbsolutePathResult(query: IPreparedQuery): Promise { - const detildifiedQuery = untildify(query.original, this.environmentService.userHome); + const detildifiedQuery = untildify(query.original, (await this.remotePathService.userHome).path); if ((await this.remotePathService.path).isAbsolute(detildifiedQuery)) { const resource = toLocalResource( await this.remotePathService.fileURI(detildifiedQuery), - this.workbenchEnvironmentService.configuration.remoteAuthority); + this.workbenchEnvironmentService.configuration.remoteAuthority + ); try { const stat = await this.fileService.resolve(resource); diff --git a/src/vs/workbench/contrib/search/browser/openSymbolHandler.ts b/src/vs/workbench/contrib/search/browser/openSymbolHandler.ts index fce5d88875..4d428bfd96 100644 --- a/src/vs/workbench/contrib/search/browser/openSymbolHandler.ts +++ b/src/vs/workbench/contrib/search/browser/openSymbolHandler.ts @@ -119,14 +119,11 @@ class SymbolEntry extends EditorQuickOpenEntry { const input: IResourceInput = { resource: this.bearing.location.uri, options: { - pinned: !this.configurationService.getValue().workbench.editor.enablePreviewFromQuickOpen + pinned: !this.configurationService.getValue().workbench.editor.enablePreviewFromQuickOpen, + selection: this.bearing.location.range ? Range.collapseToStart(this.bearing.location.range) : undefined } }; - if (this.bearing.location.range) { - input.options!.selection = Range.collapseToStart(this.bearing.location.range); - } - return input; } diff --git a/src/vs/workbench/contrib/search/browser/patternInputWidget.ts b/src/vs/workbench/contrib/search/browser/patternInputWidget.ts index 01a49653f1..1125eb9640 100644 --- a/src/vs/workbench/contrib/search/browser/patternInputWidget.ts +++ b/src/vs/workbench/contrib/search/browser/patternInputWidget.ts @@ -10,7 +10,7 @@ import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox'; import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; import { IInputValidator, HistoryInputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { KeyCode } from 'vs/base/common/keyCodes'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Event as CommonEvent, Emitter } from 'vs/base/common/event'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { attachInputBoxStyler, attachCheckboxStyler } from 'vs/platform/theme/common/styler'; @@ -170,6 +170,7 @@ export class PatternInputWidget extends Widget { case KeyCode.Escape: this._onCancel.fire(); return; + case KeyCode.Tab: case KeyCode.Tab | KeyMod.Shift: return; default: if (this.searchConfig.searchOnType) { this._onCancel.fire(); diff --git a/src/vs/workbench/contrib/search/browser/replaceService.ts b/src/vs/workbench/contrib/search/browser/replaceService.ts index 45638879e2..20fe632e3c 100644 --- a/src/vs/workbench/contrib/search/browser/replaceService.ts +++ b/src/vs/workbench/contrib/search/browser/replaceService.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import * as errors from 'vs/base/common/errors'; import { URI } from 'vs/base/common/uri'; import * as network from 'vs/base/common/network'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -19,7 +18,7 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { ScrollType } from 'vs/editor/common/editorCommon'; import { ITextModel, IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ResourceTextEdit } from 'vs/editor/common/modes'; +import { WorkspaceTextEdit } from 'vs/editor/common/modes'; import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; @@ -65,21 +64,19 @@ class ReplacePreviewModel extends Disposable { super(); } - resolve(replacePreviewUri: URI): Promise { + async resolve(replacePreviewUri: URI): Promise { const fileResource = toFileResource(replacePreviewUri); const fileMatch = this.searchWorkbenchService.searchModel.searchResult.matches().filter(match => match.resource.toString() === fileResource.toString())[0]; - return this.textModelResolverService.createModelReference(fileResource).then(ref => { - ref = this._register(ref); - const sourceModel = ref.object.textEditorModel; - const sourceModelModeId = sourceModel.getLanguageIdentifier().language; - const replacePreviewModel = this.modelService.createModel(createTextBufferFactoryFromSnapshot(sourceModel.createSnapshot()), this.modeService.create(sourceModelModeId), replacePreviewUri); - this._register(fileMatch.onChange(modelChange => this.update(sourceModel, replacePreviewModel, fileMatch, modelChange))); - this._register(this.searchWorkbenchService.searchModel.onReplaceTermChanged(() => this.update(sourceModel, replacePreviewModel, fileMatch))); - this._register(fileMatch.onDispose(() => replacePreviewModel.dispose())); // TODO@Sandeep we should not dispose a model directly but rather the reference (depends on https://github.com/Microsoft/vscode/issues/17073) - this._register(replacePreviewModel.onWillDispose(() => this.dispose())); - this._register(sourceModel.onWillDispose(() => this.dispose())); - return replacePreviewModel; - }); + const ref = this._register(await this.textModelResolverService.createModelReference(fileResource)); + const sourceModel = ref.object.textEditorModel; + const sourceModelModeId = sourceModel.getLanguageIdentifier().language; + const replacePreviewModel = this.modelService.createModel(createTextBufferFactoryFromSnapshot(sourceModel.createSnapshot()), this.modeService.create(sourceModelModeId), replacePreviewUri); + this._register(fileMatch.onChange(modelChange => this.update(sourceModel, replacePreviewModel, fileMatch, modelChange))); + this._register(this.searchWorkbenchService.searchModel.onReplaceTermChanged(() => this.update(sourceModel, replacePreviewModel, fileMatch))); + this._register(fileMatch.onDispose(() => replacePreviewModel.dispose())); // TODO@Sandeep we should not dispose a model directly but rather the reference (depends on https://github.com/Microsoft/vscode/issues/17073) + this._register(replacePreviewModel.onWillDispose(() => this.dispose())); + this._register(sourceModel.onWillDispose(() => this.dispose())); + return replacePreviewModel; } private update(sourceModel: ITextModel, replacePreviewModel: ITextModel, fileMatch: FileMatch, override: boolean = false): void { @@ -103,15 +100,24 @@ export class ReplaceService implements IReplaceService { replace(match: Match): Promise; replace(files: FileMatch[], progress?: IProgress): Promise; replace(match: FileMatchOrMatch, progress?: IProgress, resource?: URI): Promise; - replace(arg: any, progress: IProgress | undefined = undefined, resource: URI | null = null): Promise { - const edits: ResourceTextEdit[] = this.createEdits(arg, resource); - return this.bulkEditorService.apply({ edits }, { progress }).then(() => this.textFileService.saveAll(edits.map(e => e.resource))); + async replace(arg: any, progress: IProgress | undefined = undefined, resource: URI | null = null): Promise { + const edits: WorkspaceTextEdit[] = this.createEdits(arg, resource); + await this.bulkEditorService.apply({ edits }, { progress }); + + return Promise.all(edits.map(e => { + const model = this.textFileService.files.get(e.resource); + if (model) { + return model.save(); + } + + return Promise.resolve(undefined); + })); } - openReplacePreview(element: FileMatchOrMatch, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): Promise { + async openReplacePreview(element: FileMatchOrMatch, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): Promise { const fileMatch = element instanceof Match ? element.parent() : element; - return this.editorService.openEditor({ + const editor = await this.editorService.openEditor({ leftResource: fileMatch.resource, rightResource: toReplaceResource(fileMatch.resource), label: nls.localize('fileReplaceChanges', "{0} ↔ {1} (Replace Preview)", fileMatch.name(), fileMatch.name()), @@ -120,45 +126,39 @@ export class ReplaceService implements IReplaceService { pinned, revealIfVisible: true } - }).then(editor => { - const disposable = fileMatch.onDispose(() => { - if (editor && editor.input) { - editor.input.dispose(); - } - disposable.dispose(); - }); - this.updateReplacePreview(fileMatch).then(() => { - if (editor) { - const editorControl = editor.getControl(); - if (element instanceof Match && editorControl) { - editorControl.revealLineInCenter(element.range().startLineNumber, ScrollType.Immediate); - } - } - }); - }, errors.onUnexpectedError); + }); + const input = editor?.input; + const disposable = fileMatch.onDispose(() => { + if (input) { + input.dispose(); + } + disposable.dispose(); + }); + await this.updateReplacePreview(fileMatch); + if (editor) { + const editorControl = editor.getControl(); + if (element instanceof Match && editorControl) { + editorControl.revealLineInCenter(element.range().startLineNumber, ScrollType.Immediate); + } + } } - updateReplacePreview(fileMatch: FileMatch, override: boolean = false): Promise { + async updateReplacePreview(fileMatch: FileMatch, override: boolean = false): Promise { const replacePreviewUri = toReplaceResource(fileMatch.resource); - return Promise.all([this.textModelResolverService.createModelReference(fileMatch.resource), this.textModelResolverService.createModelReference(replacePreviewUri)]) - .then(([sourceModelRef, replaceModelRef]) => { - const sourceModel = sourceModelRef.object.textEditorModel; - const replaceModel = replaceModelRef.object.textEditorModel; - const returnValue = Promise.resolve(null); - // If model is disposed do not update - if (sourceModel && replaceModel) { - if (override) { - replaceModel.setValue(sourceModel.getValue()); - } else { - replaceModel.undo(); - } - this.applyEditsToPreview(fileMatch, replaceModel); - } - return returnValue.then(() => { - sourceModelRef.dispose(); - replaceModelRef.dispose(); - }); - }); + const [sourceModelRef, replaceModelRef] = await Promise.all([this.textModelResolverService.createModelReference(fileMatch.resource), this.textModelResolverService.createModelReference(replacePreviewUri)]); + const sourceModel = sourceModelRef.object.textEditorModel; + const replaceModel = replaceModelRef.object.textEditorModel; + // If model is disposed do not update + if (sourceModel && replaceModel) { + if (override) { + replaceModel.setValue(sourceModel.getValue()); + } else { + replaceModel.undo(); + } + this.applyEditsToPreview(fileMatch, replaceModel); + } + sourceModelRef.dispose(); + replaceModelRef.dispose(); } private applyEditsToPreview(fileMatch: FileMatch, replaceModel: ITextModel): void { @@ -173,8 +173,8 @@ export class ReplaceService implements IReplaceService { replaceModel.pushEditOperations([], mergeSort(modelEdits, (a, b) => Range.compareRangesUsingStarts(a.range, b.range)), () => []); } - private createEdits(arg: FileMatchOrMatch | FileMatch[], resource: URI | null = null): ResourceTextEdit[] { - const edits: ResourceTextEdit[] = []; + private createEdits(arg: FileMatchOrMatch | FileMatch[], resource: URI | null = null): WorkspaceTextEdit[] { + const edits: WorkspaceTextEdit[] = []; if (arg instanceof Match) { const match = arg; @@ -197,9 +197,9 @@ export class ReplaceService implements IReplaceService { return edits; } - private createEdit(match: Match, text: string, resource: URI | null = null): ResourceTextEdit { + private createEdit(match: Match, text: string, resource: URI | null = null): WorkspaceTextEdit { const fileMatch: FileMatch = match.parent(); - const resourceEdit: ResourceTextEdit = { + const resourceEdit: WorkspaceTextEdit = { resource: resource !== null ? resource : fileMatch.resource, edits: [{ range: match.range(), diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index 1233558354..e7f8b73ab4 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -39,7 +39,7 @@ import { ExplorerFolderContext, ExplorerRootContext, FilesExplorerFocusCondition import { OpenAnythingHandler } from 'vs/workbench/contrib/search/browser/openAnythingHandler'; import { OpenSymbolHandler } from 'vs/workbench/contrib/search/browser/openSymbolHandler'; import { registerContributions as replaceContributions } from 'vs/workbench/contrib/search/browser/replaceContributions'; -import { clearHistoryCommand, ClearSearchResultsAction, CloseReplaceAction, CollapseDeepestExpandedLevelAction, copyAllCommand, copyMatchCommand, copyPathCommand, FocusNextInputAction, FocusNextSearchResultAction, FocusPreviousInputAction, FocusPreviousSearchResultAction, focusSearchListCommand, getSearchView, openSearchView, OpenSearchViewletAction, RefreshAction, RemoveAction, ReplaceAction, ReplaceAllAction, ReplaceAllInFolderAction, ReplaceInFilesAction, toggleCaseSensitiveCommand, toggleRegexCommand, toggleWholeWordCommand, FindInFilesCommand, ToggleSearchOnTypeAction, OpenResultsInEditorAction, RerunEditorSearchAction, RerunEditorSearchWithContextAction, ExpandAllAction } from 'vs/workbench/contrib/search/browser/searchActions'; +import { clearHistoryCommand, ClearSearchResultsAction, CloseReplaceAction, CollapseDeepestExpandedLevelAction, copyAllCommand, copyMatchCommand, copyPathCommand, FocusNextInputAction, FocusNextSearchResultAction, FocusPreviousInputAction, FocusPreviousSearchResultAction, focusSearchListCommand, getSearchView, openSearchView, OpenSearchViewletAction, RefreshAction, RemoveAction, ReplaceAction, ReplaceAllAction, ReplaceAllInFolderAction, ReplaceInFilesAction, toggleCaseSensitiveCommand, toggleRegexCommand, toggleWholeWordCommand, FindInFilesCommand, ToggleSearchOnTypeAction, OpenResultsInEditorAction, ExpandAllAction, OpenSearchEditorAction, toggleSearchEditorCaseSensitiveCommand, toggleSearchEditorWholeWordCommand, toggleSearchEditorRegexCommand, toggleSearchEditorContextLinesCommand } from 'vs/workbench/contrib/search/browser/searchActions'; import { SearchPanel } from 'vs/workbench/contrib/search/browser/searchPanel'; import { SearchView, SearchViewPosition } from 'vs/workbench/contrib/search/browser/searchView'; import { registerContributions as searchWidgetContributions } from 'vs/workbench/contrib/search/browser/searchWidget'; @@ -53,9 +53,13 @@ import { ISearchConfiguration, ISearchConfigurationProperties, PANEL_ID, VIEWLET import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ExplorerViewPaneContainer } from 'vs/workbench/contrib/files/browser/explorerViewlet'; -import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { assertType } from 'vs/base/common/types'; import { SearchViewPaneContainer } from 'vs/workbench/contrib/search/browser/searchViewlet'; +import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor'; +import { SearchEditorInput, SearchEditorInputFactory, SearchEditorContribution } from 'vs/workbench/contrib/search/browser/searchEditorCommands'; +import { SearchEditor } from 'vs/workbench/contrib/search/browser/searchEditor'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { Extensions as EditorInputExtensions, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; registerSingleton(ISearchWorkbenchService, SearchWorkbenchService, true); registerSingleton(ISearchHistoryService, SearchHistoryService, true); @@ -195,7 +199,9 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: FocusNextInputAction.ID, weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.InputBoxFocusedKey), + when: ContextKeyExpr.or( + ContextKeyExpr.and(Constants.InSearchEditor, Constants.InputBoxFocusedKey), + ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.InputBoxFocusedKey)), primary: KeyMod.CtrlCmd | KeyCode.DownArrow, handler: (accessor, args: any) => { accessor.get(IInstantiationService).createInstance(FocusNextInputAction, FocusNextInputAction.ID, '').run(); @@ -205,7 +211,9 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: FocusPreviousInputAction.ID, weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.InputBoxFocusedKey, Constants.SearchInputBoxFocusedKey.toNegated()), + when: ContextKeyExpr.or( + ContextKeyExpr.and(Constants.InSearchEditor, Constants.InputBoxFocusedKey), + ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.InputBoxFocusedKey, Constants.SearchInputBoxFocusedKey.toNegated())), primary: KeyMod.CtrlCmd | KeyCode.UpArrow, handler: (accessor, args: any) => { accessor.get(IInstantiationService).createInstance(FocusPreviousInputAction, FocusPreviousInputAction.ID, '').run(); @@ -507,7 +515,7 @@ class ShowAllSymbolsAction extends Action { const viewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ id: VIEWLET_ID, name: nls.localize('name', "Search"), - ctorDescriptor: { ctor: SearchViewPaneContainer }, + ctorDescriptor: new SyncDescriptor(SearchViewPaneContainer), hideIfEmpty: true, icon: 'codicon-search', order: 1 @@ -545,7 +553,7 @@ class RegisterSearchViewContribution implements IWorkbenchContribution { } } else { Registry.as(PanelExtensions.Panels).deregisterPanel(PANEL_ID); - viewsRegistry.registerViews([{ id: VIEW_ID, name: nls.localize('search', "Search"), ctorDescriptor: { ctor: SearchView, arguments: [SearchViewPosition.SideBar] }, canToggleVisibility: false }], viewContainer); + viewsRegistry.registerViews([{ id: VIEW_ID, name: nls.localize('search', "Search"), ctorDescriptor: new SyncDescriptor(SearchView, [SearchViewPosition.SideBar]), canToggleVisibility: false }], viewContainer); if (open) { viewletService.openViewlet(VIEWLET_ID); } @@ -619,6 +627,36 @@ KeybindingsRegistry.registerCommandAndKeybindingRule(objects.assign({ handler: toggleRegexCommand }, ToggleRegexKeybinding)); +KeybindingsRegistry.registerCommandAndKeybindingRule(objects.assign({ + id: Constants.ToggleSearchEditorCaseSensitiveCommandId, + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(Constants.InSearchEditor, Constants.SearchInputBoxFocusedKey), + handler: toggleSearchEditorCaseSensitiveCommand +}, ToggleCaseSensitiveKeybinding)); + +KeybindingsRegistry.registerCommandAndKeybindingRule(objects.assign({ + id: Constants.ToggleSearchEditorWholeWordCommandId, + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(Constants.InSearchEditor, Constants.SearchInputBoxFocusedKey), + handler: toggleSearchEditorWholeWordCommand +}, ToggleWholeWordKeybinding)); + +KeybindingsRegistry.registerCommandAndKeybindingRule(objects.assign({ + id: Constants.ToggleSearchEditorRegexCommandId, + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(Constants.InSearchEditor, Constants.SearchInputBoxFocusedKey), + handler: toggleSearchEditorRegexCommand +}, ToggleRegexKeybinding)); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: Constants.ToggleSearchEditorContextLinesCommandId, + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(Constants.InSearchEditor), + handler: toggleSearchEditorContextLinesCommand, + primary: KeyMod.Alt | KeyCode.KEY_L, + mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_L } +}); + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: Constants.AddCursorsAtSearchResults, weight: KeybindingWeight.WorkbenchContrib, @@ -647,19 +685,10 @@ registry.registerWorkbenchAction( 'Search: Open Results in Editor', category, ContextKeyExpr.and(Constants.EnableSearchEditorPreview)); -const searchEditorCategory = nls.localize({ comment: ['The name of the tabbed search view'], key: 'searcheditor' }, "Search Editor"); registry.registerWorkbenchAction( - SyncActionDescriptor.create(RerunEditorSearchAction, RerunEditorSearchAction.ID, RerunEditorSearchAction.LABEL, - { primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.KEY_R }, - ContextKeyExpr.and(EditorContextKeys.languageId.isEqualTo('search-result'))), - 'Search Editor: Search Again', searchEditorCategory, - ContextKeyExpr.and(EditorContextKeys.languageId.isEqualTo('search-result'))); - -registry.registerWorkbenchAction( - SyncActionDescriptor.create(RerunEditorSearchWithContextAction, RerunEditorSearchWithContextAction.ID, RerunEditorSearchWithContextAction.LABEL), - 'Search Editor: Search Again (With Context)', searchEditorCategory, - ContextKeyExpr.and(EditorContextKeys.languageId.isEqualTo('search-result'))); - + SyncActionDescriptor.create(OpenSearchEditorAction, OpenSearchEditorAction.ID, OpenSearchEditorAction.LABEL), + 'Search: Open new Search Editor', category, + ContextKeyExpr.and(Constants.EnableSearchEditorPreview)); // Register Quick Open Handler Registry.as(QuickOpenExtensions.Quickopen).registerDefaultQuickOpenHandler( @@ -828,6 +857,17 @@ configurationRegistry.registerConfiguration({ default: false, description: nls.localize('search.enableSearchEditorPreview', "Experimental: When enabled, allows opening workspace search results in an editor.") }, + 'search.searchEditorPreview.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."), + ], + 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.") + }, 'search.sortOrder': { 'type': 'string', 'enum': [SearchSortOrder.Default, SearchSortOrder.FileNames, SearchSortOrder.Type, SearchSortOrder.Modified, SearchSortOrder.CountDescending, SearchSortOrder.CountAscending], @@ -872,3 +912,22 @@ MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { }, order: 2 }); + + +Registry.as(EditorExtensions.Editors).registerEditor( + EditorDescriptor.create( + SearchEditor, + SearchEditor.ID, + nls.localize('defaultPreferencesEditor', "Search Editor") + ), + [ + new SyncDescriptor(SearchEditorInput) + ] +); + +Registry.as(EditorInputExtensions.EditorInputFactories).registerEditorInputFactory( + SearchEditorInput.ID, + SearchEditorInputFactory); + +const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); +workbenchContributionsRegistry.registerWorkbenchContribution(SearchEditorContribution, LifecyclePhase.Starting); diff --git a/src/vs/workbench/contrib/search/browser/searchActions.ts b/src/vs/workbench/contrib/search/browser/searchActions.ts index 7d99f67fae..0306c8edba 100644 --- a/src/vs/workbench/contrib/search/browser/searchActions.ts +++ b/src/vs/workbench/contrib/search/browser/searchActions.ts @@ -29,10 +29,9 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { SearchViewPaneContainer } from 'vs/workbench/contrib/search/browser/searchViewlet'; import { SearchPanel } from 'vs/workbench/contrib/search/browser/searchPanel'; import { ITreeNavigator } from 'vs/base/browser/ui/tree/tree'; -import { createEditorFromSearchResult, refreshActiveEditorSearch } from 'vs/workbench/contrib/search/browser/searchEditor'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { createEditorFromSearchResult, openNewSearchEditor, SearchEditorInput } from 'vs/workbench/contrib/search/browser/searchEditorCommands'; + +import type { SearchEditor } from 'vs/workbench/contrib/search/browser/searchEditor'; export function isSearchViewFocused(viewletService: IViewletService, panelService: IPanelService): boolean { const searchView = getSearchView(viewletService, panelService); @@ -86,14 +85,29 @@ export const toggleCaseSensitiveCommand = (accessor: ServicesAccessor) => { } }; +export const toggleSearchEditorCaseSensitiveCommand = (accessor: ServicesAccessor) => { + const editorService = accessor.get(IEditorService); + const input = editorService.activeEditor; + if (input instanceof SearchEditorInput) { + (editorService.activeControl as SearchEditor).toggleCaseSensitive(); + } +}; + export const toggleWholeWordCommand = (accessor: ServicesAccessor) => { const searchView = getSearchView(accessor.get(IViewletService), accessor.get(IPanelService)); if (searchView) { - searchView.toggleWholeWords(); } }; +export const toggleSearchEditorWholeWordCommand = (accessor: ServicesAccessor) => { + const editorService = accessor.get(IEditorService); + const input = editorService.activeEditor; + if (input instanceof SearchEditorInput) { + (editorService.activeControl as SearchEditor).toggleWholeWords(); + } +}; + export const toggleRegexCommand = (accessor: ServicesAccessor) => { const searchView = getSearchView(accessor.get(IViewletService), accessor.get(IPanelService)); if (searchView) { @@ -101,23 +115,45 @@ export const toggleRegexCommand = (accessor: ServicesAccessor) => { } }; +export const toggleSearchEditorRegexCommand = (accessor: ServicesAccessor) => { + const editorService = accessor.get(IEditorService); + const input = editorService.activeEditor; + if (input instanceof SearchEditorInput) { + (editorService.activeControl as SearchEditor).toggleRegex(); + } +}; + +export const toggleSearchEditorContextLinesCommand = (accessor: ServicesAccessor) => { + const editorService = accessor.get(IEditorService); + const input = editorService.activeEditor; + if (input instanceof SearchEditorInput) { + (editorService.activeControl as SearchEditor).toggleContextLines(); + } +}; + export class FocusNextInputAction extends Action { static readonly ID = 'search.focus.nextInputBox'; constructor(id: string, label: string, @IViewletService private readonly viewletService: IViewletService, - @IPanelService private readonly panelService: IPanelService + @IPanelService private readonly panelService: IPanelService, + @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. + (this.editorService.activeControl as SearchEditor).focusNextInput(); + } + const searchView = getSearchView(this.viewletService, this.panelService); if (searchView) { searchView.focusNextInputBox(); } - return Promise.resolve(null); } } @@ -127,17 +163,23 @@ export class FocusPreviousInputAction extends Action { constructor(id: string, label: string, @IViewletService private readonly viewletService: IViewletService, - @IPanelService private readonly panelService: IPanelService + @IPanelService private readonly panelService: IPanelService, + @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. + (this.editorService.activeControl as SearchEditor).focusPrevInput(); + } + const searchView = getSearchView(this.viewletService, this.panelService); if (searchView) { searchView.focusPreviousInputBox(); } - return Promise.resolve(null); } } @@ -508,6 +550,30 @@ export class CancelSearchAction extends Action { } } +export class OpenSearchEditorAction extends Action { + + static readonly ID: string = Constants.OpenNewEditorCommandId; + static readonly LABEL = nls.localize('search.openNewEditor', "Open new Search Editor"); + + constructor(id: string, label: string, + @IEditorService private editorService: IEditorService, + @IConfigurationService private configurationService: IConfigurationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(id, label); + } + + get enabled(): boolean { + return true; + } + + async run() { + if (this.configurationService.getValue('search').enableSearchEditorPreview) { + await openNewSearchEditor(this.editorService, this.instantiationService); + } + } +} + export class OpenResultsInEditorAction extends Action { static readonly ID: string = Constants.OpenInEditorCommandId; @@ -518,7 +584,8 @@ export class OpenResultsInEditorAction extends Action { @IPanelService private panelService: IPanelService, @ILabelService private labelService: ILabelService, @IEditorService private editorService: IEditorService, - @IConfigurationService private configurationService: IConfigurationService + @IConfigurationService private configurationService: IConfigurationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(id, label, 'codicon-go-to-file'); } @@ -535,62 +602,7 @@ export class OpenResultsInEditorAction extends Action { async run() { const searchView = getSearchView(this.viewletService, this.panelService); if (searchView && this.configurationService.getValue('search').enableSearchEditorPreview) { - await createEditorFromSearchResult(searchView.searchResult, searchView.searchIncludePattern.getValue(), searchView.searchExcludePattern.getValue(), this.labelService, this.editorService); - } - } -} - -export class RerunEditorSearchAction extends Action { - - static readonly ID: string = Constants.RerunEditorSearchCommandId; - static readonly LABEL = nls.localize('search.rerunEditorSearch', "Search Again"); - - constructor(id: string, label: string, - @IInstantiationService private instantiationService: IInstantiationService, - @IEditorService private editorService: IEditorService, - @IConfigurationService private configurationService: IConfigurationService, - @IWorkspaceContextService private contextService: IWorkspaceContextService, - @ILabelService private labelService: ILabelService, - @IProgressService private progressService: IProgressService - ) { - super(id, label); - } - - async run() { - if (this.configurationService.getValue('search').enableSearchEditorPreview) { - await this.progressService.withProgress({ location: ProgressLocation.Window, title: nls.localize('searchRunning', "Running search...") }, - () => refreshActiveEditorSearch(undefined, this.editorService, this.instantiationService, this.contextService, this.labelService, this.configurationService)); - } - } -} - -export class RerunEditorSearchWithContextAction extends Action { - - static readonly ID: string = Constants.RerunEditorSearchWithContextCommandId; - static readonly LABEL = nls.localize('search.rerunEditorSearchContext', "Search Again (With Context)"); - - constructor(id: string, label: string, - @IInstantiationService private instantiationService: IInstantiationService, - @IEditorService private editorService: IEditorService, - @IConfigurationService private configurationService: IConfigurationService, - @IWorkspaceContextService private contextService: IWorkspaceContextService, - @ILabelService private labelService: ILabelService, - @IProgressService private progressService: IProgressService, - @IQuickInputService private quickPickService: IQuickInputService - ) { - super(id, label); - } - - async run() { - const lines = await this.quickPickService.input({ - prompt: nls.localize('lines', "Lines of Context"), - value: '2', - validateInput: async (value) => isNaN(parseInt(value)) ? nls.localize('mustBeInteger', "Must enter an integer") : undefined - }); - if (lines === undefined) { return; } - if (this.configurationService.getValue('search').enableSearchEditorPreview) { - await this.progressService.withProgress({ location: ProgressLocation.Window, title: nls.localize('searchRunning', "Running search...") }, - () => refreshActiveEditorSearch(+lines, this.editorService, this.instantiationService, this.contextService, this.labelService, this.configurationService)); + await createEditorFromSearchResult(searchView.searchResult, searchView.searchIncludePattern.getValue(), searchView.searchExcludePattern.getValue(), this.labelService, this.editorService, this.instantiationService); } } } diff --git a/src/vs/workbench/contrib/search/browser/searchEditor.ts b/src/vs/workbench/contrib/search/browser/searchEditor.ts index 612b499f5e..b77d482246 100644 --- a/src/vs/workbench/contrib/search/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/search/browser/searchEditor.ts @@ -3,336 +3,405 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Match, searchMatchComparer, FileMatch, SearchResult, SearchModel } from 'vs/workbench/contrib/search/common/searchModel'; -import { repeat } from 'vs/base/common/strings'; -import { ILabelService } from 'vs/platform/label/common/label'; -import { coalesce, flatten } from 'vs/base/common/arrays'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import * as DOM from 'vs/base/browser/dom'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { assertIsDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; -import { ITextQuery, IPatternInfo, ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search'; -import * as network from 'vs/base/common/network'; +import 'vs/css!./media/searchEditor'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; +import type { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { Range } from 'vs/editor/common/core/range'; -import { ITextModel, TrackedRangeStickiness, EndOfLinePreference } from 'vs/editor/common/model'; +import { TrackedRangeStickiness } from 'vs/editor/common/model'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { localize } from 'vs/nls'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorInput, EditorOptions } from 'vs/workbench/common/editor'; +import { ExcludePatternInputWidget, PatternInputWidget } from 'vs/workbench/contrib/search/browser/patternInputWidget'; +import { SearchWidget } from 'vs/workbench/contrib/search/browser/searchWidget'; import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder'; import { getOutOfWorkspaceEditorResources } from 'vs/workbench/contrib/search/common/search'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { searchEditorFindMatch, searchEditorFindMatchBorder } from 'vs/platform/theme/common/colorRegistry'; -import { UntitledTextEditorInput } from 'vs/workbench/common/editor/untitledTextEditorInput'; -import { localize } from 'vs/nls'; +import { SearchModel } from 'vs/workbench/contrib/search/common/searchModel'; +import { IPatternInfo, ISearchConfigurationProperties, ITextQuery } from 'vs/workbench/services/search/common/search'; +import { Delayer } from 'vs/base/common/async'; +import { serializeSearchResultForEditor, SearchConfiguration, SearchEditorInput } from 'vs/workbench/contrib/search/browser/searchEditorCommands'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { InSearchEditor, InputBoxFocusedKey } from 'vs/workbench/contrib/search/common/constants'; +import { IEditorProgressService, LongRunningOperation } from 'vs/platform/progress/common/progress'; + +const RESULT_LINE_REGEX = /^(\s+)(\d+)(:| )(\s+)(.*)$/; // Using \r\n on Windows inserts an extra newline between results. const lineDelimiter = '\n'; -const translateRangeLines = - (n: number) => - (range: Range) => - new Range(range.startLineNumber + n, range.startColumn, range.endLineNumber + n, range.endColumn); - -const matchToSearchResultFormat = (match: Match): { line: string, ranges: Range[], lineNumber: string }[] => { - const getLinePrefix = (i: number) => `${match.range().startLineNumber + i}`; - - const fullMatchLines = match.fullPreviewLines(); - const largestPrefixSize = fullMatchLines.reduce((largest, _, i) => Math.max(getLinePrefix(i).length, largest), 0); - const results: { line: string, ranges: Range[], lineNumber: string }[] = []; +export class SearchEditor extends BaseEditor { + static readonly ID: string = 'workbench.editor.searchEditor'; - fullMatchLines - .forEach((sourceLine, i) => { - const lineNumber = getLinePrefix(i); - const paddingStr = repeat(' ', largestPrefixSize - lineNumber.length); - const prefix = ` ${lineNumber}: ${paddingStr}`; - const prefixOffset = prefix.length; + private queryEditorWidget!: SearchWidget; + private searchResultEditor!: CodeEditorWidget; + private queryEditorContainer!: HTMLElement; + private dimension?: DOM.Dimension; + private inputPatternIncludes!: PatternInputWidget; + private inputPatternExcludes!: ExcludePatternInputWidget; + private includesExcludesContainer!: HTMLElement; + private toggleQueryDetailsButton!: HTMLElement; - const line = (prefix + sourceLine).replace(/\r?\n?$/, ''); + private runSearchDelayer = new Delayer(300); + private pauseSearching: boolean = false; + private showingIncludesExcludes: boolean = false; + private inSearchEditorContextKey: IContextKey; + private inputFocusContextKey: IContextKey; + private searchOperation: LongRunningOperation; + private searchHistoryDelayer: Delayer; - const rangeOnThisLine = ({ start, end }: { start?: number; end?: number; }) => new Range(1, (start ?? 1) + prefixOffset, 1, (end ?? sourceLine.length + 1) + prefixOffset); + constructor( + @ITelemetryService telemetryService: ITelemetryService, + @IThemeService themeService: IThemeService, + @IStorageService storageService: IStorageService, + @IModelService private readonly modelService: IModelService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, + @ILabelService private readonly labelService: ILabelService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IContextViewService private readonly contextViewService: IContextViewService, + @ICommandService private readonly commandService: ICommandService, + @ITextFileService private readonly textFileService: ITextFileService, + @IContextKeyService readonly contextKeyService: IContextKeyService, + @IEditorProgressService readonly progressService: IEditorProgressService, + ) { + super(SearchEditor.ID, telemetryService, themeService, storageService); + this.inSearchEditorContextKey = InSearchEditor.bindTo(contextKeyService); + this.inputFocusContextKey = InputBoxFocusedKey.bindTo(contextKeyService); + this.searchOperation = this._register(new LongRunningOperation(progressService)); + this.searchHistoryDelayer = new Delayer(2000); + } - const matchRange = match.range(); - const matchIsSingleLine = matchRange.startLineNumber === matchRange.endLineNumber; + createEditor(parent: HTMLElement) { + DOM.addClass(parent, 'search-editor'); - let lineRange; - if (matchIsSingleLine) { lineRange = (rangeOnThisLine({ start: matchRange.startColumn, end: matchRange.endColumn })); } - else if (i === 0) { lineRange = (rangeOnThisLine({ start: matchRange.startColumn })); } - else if (i === fullMatchLines.length - 1) { lineRange = (rangeOnThisLine({ end: matchRange.endColumn })); } - else { lineRange = (rangeOnThisLine({})); } + // Query + this.queryEditorContainer = DOM.append(parent, DOM.$('.query-container')); - results.push({ lineNumber: lineNumber, line, ranges: [lineRange] }); + this.queryEditorWidget = this._register(this.instantiationService.createInstance(SearchWidget, this.queryEditorContainer, { _hideReplaceToggle: true, showContextToggle: true })); + this._register(this.queryEditorWidget.onReplaceToggled(() => this.reLayout())); + this._register(this.queryEditorWidget.onDidHeightChange(() => this.reLayout())); + this.queryEditorWidget.onSearchSubmit(() => this.runSearch(true)); // onSearchSubmit has an internal delayer, so skip over ours. + this.queryEditorWidget.searchInput.onDidOptionChange(() => this.runSearch()); + this.queryEditorWidget.onDidToggleContext(() => this.runSearch()); + + // Includes/Excludes Dropdown + this.includesExcludesContainer = DOM.append(this.queryEditorContainer, DOM.$('.includes-excludes')); + // // Toggle query details button + this.toggleQueryDetailsButton = DOM.append(this.includesExcludesContainer, DOM.$('.expand.codicon.codicon-ellipsis', { tabindex: 0, role: 'button', title: localize('moreSearch', "Toggle Search Details") })); + this._register(DOM.addDisposableListener(this.toggleQueryDetailsButton, DOM.EventType.CLICK, e => { + DOM.EventHelper.stop(e); + this.toggleIncludesExcludes(); + })); + this._register(DOM.addDisposableListener(this.toggleQueryDetailsButton, DOM.EventType.KEY_UP, (e: KeyboardEvent) => { + const event = new StandardKeyboardEvent(e); + + if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { + DOM.EventHelper.stop(e); + this.toggleIncludesExcludes(); + } + })); + this._register(DOM.addDisposableListener(this.toggleQueryDetailsButton, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => { + const event = new StandardKeyboardEvent(e); + + if (event.equals(KeyMod.Shift | KeyCode.Tab)) { + if (this.queryEditorWidget.isReplaceActive()) { + this.queryEditorWidget.focusReplaceAllAction(); + } else { + this.queryEditorWidget.isReplaceShown() ? this.queryEditorWidget.replaceInput.focusOnPreserve() : this.queryEditorWidget.focusRegexAction(); + } + DOM.EventHelper.stop(e); + } + })); + + // // Includes + + const folderIncludesList = DOM.append(this.includesExcludesContainer, DOM.$('.file-types.includes')); + const filesToIncludeTitle = localize('searchScope.includes', "files to include"); + DOM.append(folderIncludesList, DOM.$('h4', undefined, filesToIncludeTitle)); + + this.inputPatternIncludes = this._register(this.instantiationService.createInstance(PatternInputWidget, folderIncludesList, this.contextViewService, { + ariaLabel: localize('label.includes', 'Search Include Patterns'), + })); + this.inputPatternIncludes.onSubmit(_triggeredOnType => this.runSearch()); + + // // Excludes + const excludesList = DOM.append(this.includesExcludesContainer, DOM.$('.file-types.excludes')); + const excludesTitle = localize('searchScope.excludes', "files to exclude"); + DOM.append(excludesList, DOM.$('h4', undefined, excludesTitle)); + + this.inputPatternExcludes = this._register(this.instantiationService.createInstance(ExcludePatternInputWidget, excludesList, this.contextViewService, { + ariaLabel: localize('label.excludes', 'Search Exclude Patterns'), + })); + + this.inputPatternExcludes.onSubmit(_triggeredOnType => this.runSearch()); + this.inputPatternExcludes.onChangeIgnoreBox(() => this.runSearch()); + + // Editor + const searchResultContainer = DOM.append(parent, DOM.$('.search-results')); + const configuration: IEditorOptions = this.configurationService.getValue('editor', { overrideIdentifier: 'search-result' }); + const options: ICodeEditorWidgetOptions = {}; + this.searchResultEditor = this._register(this.instantiationService.createInstance(CodeEditorWidget, searchResultContainer, configuration, options)); + this.searchResultEditor.onMouseUp(e => { + + if (e.event.detail === 2) { + const behaviour = this.configurationService.getValue('search').searchEditorPreview.doubleClickBehaviour; + const position = e.target.position; + if (position && behaviour !== 'selectWord') { + const line = this.searchResultEditor.getModel()?.getLineContent(position.lineNumber) ?? ''; + if (line.match(RESULT_LINE_REGEX)) { + this.searchResultEditor.setSelection(Range.fromPositions(position)); + this.commandService.executeCommand(behaviour === 'goToLocation' ? 'editor.action.goToDeclaration' : 'editor.action.openDeclarationToTheSide'); + } + } + } }); - return results; -}; + this._register(this.searchResultEditor.onKeyDown(e => e.keyCode === KeyCode.Escape && this.queryEditorWidget.searchInput.focus())); -type SearchResultSerialization = { text: string[], matchRanges: Range[] }; + this._register(this.searchResultEditor.onDidChangeModel(() => this.hideHeader())); + this._register(this.searchResultEditor.onDidChangeModelContent(() => (this._input as SearchEditorInput)?.setDirty(true))); -function fileMatchToSearchResultFormat(fileMatch: FileMatch, labelFormatter: (x: URI) => string): SearchResultSerialization { - const serializedMatches = flatten(fileMatch.matches() - .sort(searchMatchComparer) - .map(match => matchToSearchResultFormat(match))); - - const uriString = labelFormatter(fileMatch.resource); - let text: string[] = [`${uriString}:`]; - let matchRanges: Range[] = []; - - const targetLineNumberToOffset: Record = {}; - - const context: { line: string, lineNumber: number }[] = []; - fileMatch.context.forEach((line, lineNumber) => context.push({ line, lineNumber })); - context.sort((a, b) => a.lineNumber - b.lineNumber); - - let lastLine: number | undefined = undefined; - - const seenLines = new Set(); - serializedMatches.forEach(match => { - if (!seenLines.has(match.line)) { - while (context.length && context[0].lineNumber < +match.lineNumber) { - const { line, lineNumber } = context.shift()!; - if (lastLine !== undefined && lineNumber !== lastLine + 1) { - text.push(''); - } - text.push(` ${lineNumber} ${line}`); - lastLine = lineNumber; - } - - targetLineNumberToOffset[match.lineNumber] = text.length; - seenLines.add(match.line); - text.push(match.line); - lastLine = +match.lineNumber; - } - - matchRanges.push(...match.ranges.map(translateRangeLines(targetLineNumberToOffset[match.lineNumber]))); - }); - - while (context.length) { - const { line, lineNumber } = context.shift()!; - text.push(` ${lineNumber} ${line}`); + [this.queryEditorWidget.searchInputFocusTracker, this.queryEditorWidget.replaceInputFocusTracker, this.inputPatternExcludes.inputFocusTracker, this.inputPatternIncludes.inputFocusTracker] + .map(tracker => { + this._register(tracker.onDidFocus(() => setTimeout(() => this.inputFocusContextKey.set(true), 0))); + this._register(tracker.onDidBlur(() => this.inputFocusContextKey.set(false))); + }); } - return { text, matchRanges }; -} - -const flattenSearchResultSerializations = (serializations: SearchResultSerialization[]): SearchResultSerialization => { - let text: string[] = []; - let matchRanges: Range[] = []; - - serializations.forEach(serialized => { - serialized.matchRanges.map(translateRangeLines(text.length)).forEach(range => matchRanges.push(range)); - serialized.text.forEach(line => text.push(line)); - text.push(''); // new line - }); - - return { text, matchRanges }; -}; - -const contentPatternToSearchResultHeader = (pattern: ITextQuery | null, includes: string, excludes: string, contextLines: number): string[] => { - if (!pattern) { return []; } - - const removeNullFalseAndUndefined = (a: (T | null | false | undefined)[]) => a.filter(a => a !== false && a !== null && a !== undefined) as T[]; - - const escapeNewlines = (str: string) => str.replace(/\\/g, '\\\\').replace(/\n/g, '\\n'); - - return removeNullFalseAndUndefined([ - `# Query: ${escapeNewlines(pattern.contentPattern.pattern)}`, - - (pattern.contentPattern.isCaseSensitive || pattern.contentPattern.isWordMatch || pattern.contentPattern.isRegExp || pattern.userDisabledExcludesAndIgnoreFiles) - && `# Flags: ${coalesce([ - pattern.contentPattern.isCaseSensitive && 'CaseSensitive', - pattern.contentPattern.isWordMatch && 'WordMatch', - pattern.contentPattern.isRegExp && 'RegExp', - pattern.userDisabledExcludesAndIgnoreFiles && 'IgnoreExcludeSettings' - ]).join(' ')}`, - includes ? `# Including: ${includes}` : undefined, - excludes ? `# Excluding: ${excludes}` : undefined, - contextLines ? `# ContextLines: ${contextLines}` : undefined, - '' - ]); -}; - - -type SearchHeader = { - pattern: string; - flags: { - regex: boolean; - wholeWord: boolean; - caseSensitive: boolean; - ignoreExcludes: boolean; - }; - includes: string; - excludes: string; - context: number | undefined; -}; - -const searchHeaderToContentPattern = (header: string[]): SearchHeader => { - const query: SearchHeader = { - pattern: '', - flags: { regex: false, caseSensitive: false, ignoreExcludes: false, wholeWord: false }, - includes: '', - excludes: '', - context: undefined - }; - - const unescapeNewlines = (str: string) => { - let out = ''; - for (let i = 0; i < str.length; i++) { - if (str[i] === '\\') { - i++; - const escaped = str[i]; - - if (escaped === 'n') { - out += '\n'; - } - else if (escaped === '\\') { - out += '\\'; - } - else { - throw Error(localize('invalidQueryStringError', "All backslashes in Query string must be escaped (\\\\)")); - } + focusNextInput() { + if (this.queryEditorWidget.searchInputHasFocus()) { + if (this.showingIncludesExcludes) { + this.inputPatternIncludes.focus(); } else { - out += str[i]; - } - } - return out; - }; - const parseYML = /^# ([^:]*): (.*)$/; - for (const line of header) { - const parsed = parseYML.exec(line); - if (!parsed) { continue; } - const [, key, value] = parsed; - switch (key) { - case 'Query': query.pattern = unescapeNewlines(value); break; - case 'Including': query.includes = value; break; - case 'Excluding': query.excludes = value; break; - case 'ContextLines': query.context = +value; break; - case 'Flags': { - query.flags = { - regex: value.indexOf('RegExp') !== -1, - caseSensitive: value.indexOf('CaseSensitive') !== -1, - ignoreExcludes: value.indexOf('IgnoreExcludeSettings') !== -1, - wholeWord: value.indexOf('WordMatch') !== -1 - }; + this.searchResultEditor.focus(); } + } else if (this.inputPatternIncludes.inputHasFocus()) { + this.inputPatternExcludes.focus(); + } else if (this.inputPatternExcludes.inputHasFocus()) { + this.searchResultEditor.focus(); + } else if (this.searchResultEditor.hasWidgetFocus()) { + // pass } } - return query; -}; + focusPrevInput() { + if (this.queryEditorWidget.searchInputHasFocus()) { + this.searchResultEditor.focus(); // wrap + } else if (this.inputPatternIncludes.inputHasFocus()) { + this.queryEditorWidget.searchInput.focus(); + } else if (this.inputPatternExcludes.inputHasFocus()) { + this.inputPatternIncludes.focus(); + } else if (this.searchResultEditor.hasWidgetFocus()) { + // ureachable. + } + } -const serializeSearchResultForEditor = (searchResult: SearchResult, rawIncludePattern: string, rawExcludePattern: string, contextLines: number, labelFormatter: (x: URI) => string): SearchResultSerialization => { - const header = contentPatternToSearchResultHeader(searchResult.query, rawIncludePattern, rawExcludePattern, contextLines); - const allResults = - flattenSearchResultSerializations( - flatten( - searchResult.folderMatches().sort(searchMatchComparer) - .map(folderMatch => folderMatch.matches().sort(searchMatchComparer) - .map(fileMatch => fileMatchToSearchResultFormat(fileMatch, labelFormatter))))); + toggleWholeWords() { + this.queryEditorWidget.searchInput.setWholeWords(!this.queryEditorWidget.searchInput.getWholeWords()); + this.runSearch(); + } - return { matchRanges: allResults.matchRanges.map(translateRangeLines(header.length)), text: header.concat(allResults.text) }; -}; + toggleRegex() { + this.queryEditorWidget.searchInput.setRegex(!this.queryEditorWidget.searchInput.getRegex()); + this.runSearch(); + } -export const refreshActiveEditorSearch = - async (contextLines: number | undefined, editorService: IEditorService, instantiationService: IInstantiationService, contextService: IWorkspaceContextService, labelService: ILabelService, configurationService: IConfigurationService) => { - const model = editorService.activeTextEditorWidget?.getModel(); - if (!model) { return; } + toggleCaseSensitive() { + this.queryEditorWidget.searchInput.setCaseSensitive(!this.queryEditorWidget.searchInput.getCaseSensitive()); + this.runSearch(); + } - const textModel = model as ITextModel; + toggleContextLines() { + this.queryEditorWidget.toggleContextLines(); + } - const header = textModel.getValueInRange(new Range(1, 1, 5, 1), EndOfLinePreference.LF) - .split(lineDelimiter) - .filter(line => line.indexOf('# ') === 0); + private async runSearch(instant = false) { + if (!this.pauseSearching) { + this.runSearchDelayer.trigger(() => this.doRunSearch(), instant ? 0 : undefined); + } + } - const contentPattern = searchHeaderToContentPattern(header); + private async doRunSearch() { + const startInput = this.input; - const content: IPatternInfo = { - pattern: contentPattern.pattern, - isRegExp: contentPattern.flags.regex, - isCaseSensitive: contentPattern.flags.caseSensitive, - isWordMatch: contentPattern.flags.wholeWord + this.searchHistoryDelayer.trigger(() => { + this.queryEditorWidget.searchInput.onSearchSubmit(); + this.inputPatternExcludes.onSearchSubmit(); + this.inputPatternIncludes.onSearchSubmit(); + }); + + const config: SearchConfiguration = { + caseSensitive: this.queryEditorWidget.searchInput.getCaseSensitive(), + contextLines: this.queryEditorWidget.contextLines(), + excludes: this.inputPatternExcludes.getValue(), + includes: this.inputPatternIncludes.getValue(), + query: this.queryEditorWidget.searchInput.getValue(), + regexp: this.queryEditorWidget.searchInput.getRegex(), + wholeWord: this.queryEditorWidget.searchInput.getWholeWords(), + useIgnores: this.inputPatternExcludes.useExcludesAndIgnoreFiles(), + showIncludesExcludes: this.showingIncludesExcludes }; - contextLines = contextLines ?? contentPattern.context ?? 0; + if (!config.query) { return; } + + const content: IPatternInfo = { + pattern: config.query, + isRegExp: config.regexp, + isCaseSensitive: config.caseSensitive, + isWordMatch: config.wholeWord, + }; const options: ITextQueryBuilderOptions = { _reason: 'searchEditor', - extraFileResources: instantiationService.invokeFunction(getOutOfWorkspaceEditorResources), + extraFileResources: this.instantiationService.invokeFunction(getOutOfWorkspaceEditorResources), maxResults: 10000, - disregardIgnoreFiles: contentPattern.flags.ignoreExcludes, - disregardExcludeSettings: contentPattern.flags.ignoreExcludes, - excludePattern: contentPattern.excludes, - includePattern: contentPattern.includes, + disregardIgnoreFiles: !config.useIgnores, + disregardExcludeSettings: !config.useIgnores, + excludePattern: config.excludes, + includePattern: config.includes, previewOptions: { matchLines: 1, charsPerLine: 1000 }, - afterContext: contextLines, - beforeContext: contextLines, - isSmartCase: configurationService.getValue('search').smartCase, + afterContext: config.contextLines, + beforeContext: config.contextLines, + isSmartCase: this.configurationService.getValue('search').smartCase, expandPatterns: true }; - const folderResources = contextService.getWorkspace().folders; - + const folderResources = this.contextService.getWorkspace().folders; let query: ITextQuery; try { - const queryBuilder = instantiationService.createInstance(QueryBuilder); + const queryBuilder = this.instantiationService.createInstance(QueryBuilder); query = queryBuilder.text(content, folderResources.map(folder => folder.uri), options); - } catch (err) { + } + catch (err) { + return; + } + const searchModel = this.instantiationService.createInstance(SearchModel); + this.searchOperation.start(500); + await searchModel.search(query).finally(() => this.searchOperation.stop()); + if (this.input !== startInput) { + searchModel.dispose(); return; } - const searchModel = instantiationService.createInstance(SearchModel); - await searchModel.search(query); + (assertIsDefined(this._input) as SearchEditorInput).setConfig(config); - const labelFormatter = (uri: URI): string => labelService.getUriLabel(uri, { relative: true }); - const results = serializeSearchResultForEditor(searchModel.searchResult, contentPattern.includes, contentPattern.excludes, contextLines, labelFormatter); - - textModel.setValue(results.text.join(lineDelimiter)); + const labelFormatter = (uri: URI): string => this.labelService.getUriLabel(uri, { relative: true }); + const results = serializeSearchResultForEditor(searchModel.searchResult, config.includes, config.excludes, config.contextLines, labelFormatter, true); + const textModel = assertIsDefined(this.searchResultEditor.getModel()); + this.modelService.updateModel(textModel, results.text.join(lineDelimiter)); + this.getInput()?.setDirty(this.getInput()?.resource.scheme !== 'search-editor'); + this.hideHeader(); textModel.deltaDecorations([], results.matchRanges.map(range => ({ range, options: { className: 'searchEditorFindMatch', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } }))); - }; + searchModel.dispose(); + } -export const createEditorFromSearchResult = - async (searchResult: SearchResult, rawIncludePattern: string, rawExcludePattern: string, labelService: ILabelService, editorService: IEditorService) => { - const searchTerm = searchResult.query?.contentPattern.pattern.replace(/[^\w-_. ]/g, '') || 'Search'; + private hideHeader() { + const headerLines = + this.searchResultEditor + .getModel() + ?.getValueInRange(new Range(1, 1, 6, 1)) + .split('\n') + .filter(line => line.startsWith('#')) + .length + ?? 0; - const labelFormatter = (uri: URI): string => labelService.getUriLabel(uri, { relative: true }); + this.searchResultEditor.setHiddenAreas([new Range(1, 1, headerLines + 1, 1)]); + } - const results = serializeSearchResultForEditor(searchResult, rawIncludePattern, rawExcludePattern, 0, labelFormatter); - const contents = results.text.join(lineDelimiter); - let possible = { - contents, - mode: 'search-result', - resource: URI.from({ scheme: network.Schemas.untitled, path: searchTerm }) - }; + layout(dimension: DOM.Dimension) { + this.dimension = dimension; + this.reLayout(); + } - let id = 0; - let existing = editorService.getOpened(possible); - while (existing) { - if (existing instanceof UntitledTextEditorInput) { - const model = await existing.resolve(); - const existingContents = model.textEditorModel.getValue(EndOfLinePreference.LF); - if (existingContents === contents) { - break; - } + focusInput() { + this.queryEditorWidget.focus(); + } + + private reLayout() { + if (this.dimension) { + this.queryEditorWidget.setWidth(this.dimension.width - 28 /* container margin */); + this.searchResultEditor.layout({ height: this.dimension.height - DOM.getTotalHeight(this.queryEditorContainer), width: this.dimension.width }); + this.inputPatternExcludes.setWidth(this.dimension.width - 28 /* container margin */); + this.inputPatternIncludes.setWidth(this.dimension.width - 28 /* container margin */); + } + } + + private getInput(): SearchEditorInput | undefined { + return this._input as SearchEditorInput; + } + + async setInput(newInput: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + await super.setInput(newInput, options, token); + this.inSearchEditorContextKey.set(true); + + if (!(newInput instanceof SearchEditorInput)) { return; } + this.pauseSearching = true; + const model = this.modelService.getModel(newInput.resource); + if (newInput.resource.scheme !== 'search-editor') { + if (model?.getValue() === '') { + model.setValue((await this.textFileService.read(newInput.resource)).value); } - possible.resource = possible.resource.with({ path: searchTerm + '-' + ++id }); - existing = editorService.getOpened(possible); + } + this.searchResultEditor.setModel(model); + this.queryEditorWidget.setValue(newInput.config.query, true); + this.queryEditorWidget.searchInput.setCaseSensitive(newInput.config.caseSensitive); + this.queryEditorWidget.searchInput.setRegex(newInput.config.regexp); + this.queryEditorWidget.searchInput.setWholeWords(newInput.config.wholeWord); + this.queryEditorWidget.setContextLines(newInput.config.contextLines); + this.inputPatternExcludes.setValue(newInput.config.excludes); + this.inputPatternIncludes.setValue(newInput.config.includes); + this.inputPatternExcludes.setUseExcludesAndIgnoreFiles(newInput.config.useIgnores); + this.toggleIncludesExcludes(newInput.config.showIncludesExcludes); + + this.focusInput(); + this.pauseSearching = false; + } + + private toggleIncludesExcludes(_shouldShow?: boolean): void { + const cls = 'expanded'; + const shouldShow = _shouldShow ?? !DOM.hasClass(this.includesExcludesContainer, cls); + + if (shouldShow) { + this.toggleQueryDetailsButton.setAttribute('aria-expanded', 'true'); + DOM.addClass(this.includesExcludesContainer, cls); + } else { + this.toggleQueryDetailsButton.setAttribute('aria-expanded', 'false'); + DOM.removeClass(this.includesExcludesContainer, cls); } - const editor = await editorService.openEditor(possible); - const control = editor?.getControl()!; - const model = control.getModel() as ITextModel; + this.showingIncludesExcludes = DOM.hasClass(this.includesExcludesContainer, cls); - model.deltaDecorations([], results.matchRanges.map(range => ({ range, options: { className: 'searchEditorFindMatch', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } }))); - }; - -registerThemingParticipant((theme, collector) => { - collector.addRule(`.monaco-editor .searchEditorFindMatch { background-color: ${theme.getColor(searchEditorFindMatch)}; }`); - - const findMatchHighlightBorder = theme.getColor(searchEditorFindMatchBorder); - if (findMatchHighlightBorder) { - collector.addRule(`.monaco-editor .searchEditorFindMatch { border: 1px ${theme.type === 'hc' ? 'dotted' : 'solid'} ${findMatchHighlightBorder}; box-sizing: border-box; }`); + this.reLayout(); } -}); + + getModel() { + return this.searchResultEditor.getModel(); + } + + clearInput() { + super.clearInput(); + this.inSearchEditorContextKey.set(false); + } +} diff --git a/src/vs/workbench/contrib/search/browser/searchEditorCommands.ts b/src/vs/workbench/contrib/search/browser/searchEditorCommands.ts new file mode 100644 index 0000000000..927702194e --- /dev/null +++ b/src/vs/workbench/contrib/search/browser/searchEditorCommands.ts @@ -0,0 +1,542 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { coalesce, flatten } from 'vs/base/common/arrays'; +import * as network from 'vs/base/common/network'; +import { repeat, endsWith } from 'vs/base/common/strings'; +import { assertIsDefined } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; +import 'vs/css!./media/searchEditor'; +import { isDiffEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Range } from 'vs/editor/common/core/range'; +import { EndOfLinePreference, TrackedRangeStickiness, ITextModel } from 'vs/editor/common/model'; +import { localize } from 'vs/nls'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { searchEditorFindMatch, searchEditorFindMatchBorder } from 'vs/platform/theme/common/colorRegistry'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { UntitledTextEditorInput } from 'vs/workbench/common/editor/untitledTextEditorInput'; +import { FileMatch, Match, searchMatchComparer, SearchResult } from 'vs/workbench/contrib/search/common/searchModel'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ITextQuery } from 'vs/workbench/services/search/common/search'; +import { IEditorInputFactory, GroupIdentifier, EditorInput, SaveContext } from 'vs/workbench/common/editor'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { SearchEditor } from 'vs/workbench/contrib/search/browser/searchEditor'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { ITextFileSaveOptions, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import type { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { dirname, joinPath, isEqual } from 'vs/base/common/resources'; +import { IHistoryService } from 'vs/workbench/services/history/common/history'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { basename } from 'vs/base/common/path'; + + + +export type SearchConfiguration = { + query: string, + includes: string, + excludes: string + contextLines: number, + wholeWord: boolean, + caseSensitive: boolean, + regexp: boolean, + useIgnores: boolean, + showIncludesExcludes: boolean, +}; + +export class SearchEditorContribution implements IWorkbenchContribution { + constructor( + @IEditorService private readonly editorService: IEditorService, + @ITextFileService protected readonly textFileService: ITextFileService, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IModelService protected readonly modelService: IModelService, + ) { + + this.editorService.overrideOpenEditor((editor, options, group) => { + const resource = editor.getResource(); + if (!resource || + !(endsWith(resource.path, '.code-search') || resource.scheme === 'search-editor') || + !(editor instanceof FileEditorInput || (resource.scheme === 'search-editor'))) { + return undefined; + } + + if (group.isOpened(editor)) { + return undefined; + } + + return { + override: (async () => { + const contents = resource.scheme === 'search-editor' ? this.modelService.getModel(resource)?.getValue() ?? '' : (await this.textFileService.read(resource)).value; + const header = searchHeaderToContentPattern(contents.split('\n').slice(0, 5)); + + const input = instantiationService.createInstance( + SearchEditorInput, + { + query: header.pattern, + regexp: header.flags.regex, + caseSensitive: header.flags.caseSensitive, + wholeWord: header.flags.wholeWord, + includes: header.includes, + excludes: header.excludes, + contextLines: header.context ?? 0, + useIgnores: !header.flags.ignoreExcludes, + showIncludesExcludes: !!(header.includes || header.excludes || header.flags.ignoreExcludes) + }, contents, resource); + + return editorService.openEditor(input, { ...options, pinned: resource.scheme === 'search-editor', ignoreOverrides: true }, group); + })() + }; + }); + } +} + +export class SearchEditorInputFactory implements IEditorInputFactory { + + canSerialize() { return true; } + + serialize(input: SearchEditorInput) { + let resource = undefined; + if (input.resource.path) { + resource = input.resource.toString(); + } + + return JSON.stringify({ ...input.config, resource }); + } + + deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): SearchEditorInput | undefined { + const { resource, ...config } = JSON.parse(serializedEditorInput); + return instantiationService.createInstance(SearchEditorInput, config, undefined, resource && URI.parse(resource)); + } +} + +let searchEditorInputInstances = 0; +export class SearchEditorInput extends EditorInput { + static readonly ID: string = 'workbench.editorinputs.searchEditorInput'; + + private _config: SearchConfiguration; + public get config(): Readonly { + return this._config; + } + + private model: ITextModel; + public readonly resource: URI; + + private dirty: boolean = false; + + constructor( + config: Partial | undefined, + initialContents: string | undefined, + resource: URI | undefined, + @IModelService private readonly modelService: IModelService, + @IModeService private readonly modeService: IModeService, + @IEditorService protected readonly editorService: IEditorService, + @IEditorGroupsService protected readonly editorGroupService: IEditorGroupsService, + @ITextFileService protected readonly textFileService: ITextFileService, + @IHistoryService private readonly historyService: IHistoryService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IFileDialogService private readonly fileDialogService: IFileDialogService, + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { + super(); + this.resource = resource ?? URI.from({ scheme: 'search-editor', fragment: `${searchEditorInputInstances++}` }); + this._config = { ...{ query: '', includes: '', excludes: '', contextLines: 0, wholeWord: false, caseSensitive: false, regexp: false, useIgnores: true, showIncludesExcludes: false }, ...config }; + + const searchResultMode = this.modeService.create('search-result'); + + this.model = this.modelService.getModel(this.resource) ?? this.modelService.createModel(initialContents ?? '', searchResultMode, this.resource); + } + + async save(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise { + if (this.resource.scheme === 'search-editor') { + const path = await this.promptForPath(this.resource, this.suggestFileName(), options?.availableFileSystems); + if (path) { + if (await this.textFileService.saveAs(this.resource, path, options)) { + this.setDirty(false); + if (options?.context !== SaveContext.EDITOR_CLOSE && !isEqual(path, this.resource)) { + const replacement = this.instantiationService.createInstance(SearchEditorInput, this.config, undefined, path); + await this.editorService.replaceEditors([{ editor: this, replacement, options: { pinned: true } }], group); + return true; + } else if (options?.context === SaveContext.EDITOR_CLOSE) { + return true; + } + } + } + return false; + } else { + this.setDirty(false); + return !!this.textFileService.write(this.resource, this.model.getValue(), options); + } + } + + // Brining this over from textFileService because it only suggests for untitled scheme. + // In the future I may just use the untitled scheme. I dont get particular benefit from using search-editor... + private async promptForPath(resource: URI, defaultUri: URI, availableFileSystems?: string[]): Promise { + // Help user to find a name for the file by opening it first + await this.editorService.openEditor({ resource, options: { revealIfOpened: true, preserveFocus: true } }); + return this.fileDialogService.pickFileToSave(defaultUri, availableFileSystems); + } + + getTypeId(): string { + return SearchEditorInput.ID; + } + + getName(): string { + if (this.resource.scheme === 'search-editor') { + return this.config.query ? localize('searchTitle.withQuery', "Search: {0}", this.config.query) : localize('searchTitle', "Search"); + } + return localize('searchTitle.withQuery', "Search: {0}", basename(this.resource.path, '.code-search')); + } + + setConfig(config: SearchConfiguration) { + this._config = config; + this._onDidChangeLabel.fire(); + } + + async resolve() { + return null; + } + + setDirty(dirty: boolean) { + this.dirty = dirty; + this._onDidChangeDirty.fire(); + } + + isDirty() { + return this.dirty; + } + + dispose() { + this.modelService.destroyModel(this.resource); + super.dispose(); + } + + matches(other: unknown) { + if (this === other) { return true; } + + if (other instanceof SearchEditorInput) { + if ( + (other.resource.path && other.resource.path === this.resource.path) || + (other.resource.fragment && other.resource.fragment === this.resource.fragment) + ) { + return true; + } + } + return false; + } + + // Bringing this over from textFileService because it only suggests for untitled scheme. + // In the future I may just use the untitled scheme. I dont get particular benefit from using search-editor... + private suggestFileName(): URI { + const searchFileName = (this.config.query.replace(/[^\w \-_]+/g, '_') || 'Search') + '.code-search'; + + const remoteAuthority = this.environmentService.configuration.remoteAuthority; + const schemeFilter = remoteAuthority ? network.Schemas.vscodeRemote : network.Schemas.file; + + const lastActiveFile = this.historyService.getLastActiveFile(schemeFilter); + if (lastActiveFile) { + const lastDir = dirname(lastActiveFile); + return joinPath(lastDir, searchFileName); + } + + const lastActiveFolder = this.historyService.getLastActiveWorkspaceRoot(schemeFilter); + if (lastActiveFolder) { + return joinPath(lastActiveFolder, searchFileName); + } + + return URI.from({ scheme: schemeFilter, path: searchFileName }); + } +} + +// Using \r\n on Windows inserts an extra newline between results. +const lineDelimiter = '\n'; + +const translateRangeLines = + (n: number) => + (range: Range) => + new Range(range.startLineNumber + n, range.startColumn, range.endLineNumber + n, range.endColumn); + +const matchToSearchResultFormat = (match: Match): { line: string, ranges: Range[], lineNumber: string }[] => { + const getLinePrefix = (i: number) => `${match.range().startLineNumber + i}`; + + const fullMatchLines = match.fullPreviewLines(); + const largestPrefixSize = fullMatchLines.reduce((largest, _, i) => Math.max(getLinePrefix(i).length, largest), 0); + + + const results: { line: string, ranges: Range[], lineNumber: string }[] = []; + + fullMatchLines + .forEach((sourceLine, i) => { + const lineNumber = getLinePrefix(i); + const paddingStr = repeat(' ', largestPrefixSize - lineNumber.length); + const prefix = ` ${lineNumber}: ${paddingStr}`; + const prefixOffset = prefix.length; + + const line = (prefix + sourceLine).replace(/\r?\n?$/, ''); + + 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 matchIsSingleLine = matchRange.startLineNumber === matchRange.endLineNumber; + + let lineRange; + if (matchIsSingleLine) { lineRange = (rangeOnThisLine({ start: matchRange.startColumn, end: matchRange.endColumn })); } + else if (i === 0) { lineRange = (rangeOnThisLine({ start: matchRange.startColumn })); } + else if (i === fullMatchLines.length - 1) { lineRange = (rangeOnThisLine({ end: matchRange.endColumn })); } + else { lineRange = (rangeOnThisLine({})); } + + results.push({ lineNumber: lineNumber, line, ranges: [lineRange] }); + }); + + return results; +}; + +type SearchResultSerialization = { text: string[], matchRanges: Range[] }; + +function fileMatchToSearchResultFormat(fileMatch: FileMatch, labelFormatter: (x: URI) => string): SearchResultSerialization { + const serializedMatches = flatten(fileMatch.matches() + .sort(searchMatchComparer) + .map(match => matchToSearchResultFormat(match))); + + const uriString = labelFormatter(fileMatch.resource); + let text: string[] = [`${uriString}:`]; + let matchRanges: Range[] = []; + + const targetLineNumberToOffset: Record = {}; + + const context: { line: string, lineNumber: number }[] = []; + fileMatch.context.forEach((line, lineNumber) => context.push({ line, lineNumber })); + context.sort((a, b) => a.lineNumber - b.lineNumber); + + let lastLine: number | undefined = undefined; + + const seenLines = new Set(); + serializedMatches.forEach(match => { + if (!seenLines.has(match.line)) { + while (context.length && context[0].lineNumber < +match.lineNumber) { + const { line, lineNumber } = context.shift()!; + if (lastLine !== undefined && lineNumber !== lastLine + 1) { + text.push(''); + } + text.push(` ${lineNumber} ${line}`); + lastLine = lineNumber; + } + + targetLineNumberToOffset[match.lineNumber] = text.length; + seenLines.add(match.line); + text.push(match.line); + lastLine = +match.lineNumber; + } + + matchRanges.push(...match.ranges.map(translateRangeLines(targetLineNumberToOffset[match.lineNumber]))); + }); + + while (context.length) { + const { line, lineNumber } = context.shift()!; + text.push(` ${lineNumber} ${line}`); + } + + return { text, matchRanges }; +} + +const flattenSearchResultSerializations = (serializations: SearchResultSerialization[]): SearchResultSerialization => { + let text: string[] = []; + let matchRanges: Range[] = []; + + serializations.forEach(serialized => { + serialized.matchRanges.map(translateRangeLines(text.length)).forEach(range => matchRanges.push(range)); + serialized.text.forEach(line => text.push(line)); + text.push(''); // new line + }); + + return { text, matchRanges }; +}; + +const contentPatternToSearchResultHeader = (pattern: ITextQuery | null, includes: string, excludes: string, contextLines: number): string[] => { + if (!pattern) { return []; } + + const removeNullFalseAndUndefined = (a: (T | null | false | undefined)[]) => a.filter(a => a !== false && a !== null && a !== undefined) as T[]; + + const escapeNewlines = (str: string) => str.replace(/\\/g, '\\\\').replace(/\n/g, '\\n'); + + return removeNullFalseAndUndefined([ + `# Query: ${escapeNewlines(pattern.contentPattern.pattern)}`, + + (pattern.contentPattern.isCaseSensitive || pattern.contentPattern.isWordMatch || pattern.contentPattern.isRegExp || pattern.userDisabledExcludesAndIgnoreFiles) + && `# Flags: ${coalesce([ + pattern.contentPattern.isCaseSensitive && 'CaseSensitive', + pattern.contentPattern.isWordMatch && 'WordMatch', + pattern.contentPattern.isRegExp && 'RegExp', + pattern.userDisabledExcludesAndIgnoreFiles && 'IgnoreExcludeSettings' + ]).join(' ')}`, + includes ? `# Including: ${includes}` : undefined, + excludes ? `# Excluding: ${excludes}` : undefined, + contextLines ? `# ContextLines: ${contextLines}` : undefined, + '' + ]); +}; + + +type SearchHeader = { + pattern: string; + flags: { + regex: boolean; + wholeWord: boolean; + caseSensitive: boolean; + ignoreExcludes: boolean; + }; + includes: string; + excludes: string; + context: number | undefined; +}; + +const searchHeaderToContentPattern = (header: string[]): SearchHeader => { + const query: SearchHeader = { + pattern: '', + flags: { regex: false, caseSensitive: false, ignoreExcludes: false, wholeWord: false }, + includes: '', + excludes: '', + context: undefined + }; + + const unescapeNewlines = (str: string) => { + let out = ''; + for (let i = 0; i < str.length; i++) { + if (str[i] === '\\') { + i++; + const escaped = str[i]; + + if (escaped === 'n') { + out += '\n'; + } + else if (escaped === '\\') { + out += '\\'; + } + else { + throw Error(localize('invalidQueryStringError', "All backslashes in Query string must be escaped (\\\\)")); + } + } else { + out += str[i]; + } + } + return out; + }; + const parseYML = /^# ([^:]*): (.*)$/; + for (const line of header) { + const parsed = parseYML.exec(line); + if (!parsed) { continue; } + const [, key, value] = parsed; + switch (key) { + case 'Query': query.pattern = unescapeNewlines(value); break; + case 'Including': query.includes = value; break; + case 'Excluding': query.excludes = value; break; + case 'ContextLines': query.context = +value; break; + case 'Flags': { + query.flags = { + regex: value.indexOf('RegExp') !== -1, + caseSensitive: value.indexOf('CaseSensitive') !== -1, + ignoreExcludes: value.indexOf('IgnoreExcludeSettings') !== -1, + wholeWord: value.indexOf('WordMatch') !== -1 + }; + } + } + } + + return query; +}; + +export const serializeSearchResultForEditor = (searchResult: SearchResult, rawIncludePattern: string, rawExcludePattern: string, contextLines: number, labelFormatter: (x: URI) => string, includeHeader: boolean): SearchResultSerialization => { + const header = includeHeader ? contentPatternToSearchResultHeader(searchResult.query, rawIncludePattern, rawExcludePattern, contextLines) : []; + const allResults = + flattenSearchResultSerializations( + flatten( + searchResult.folderMatches().sort(searchMatchComparer) + .map(folderMatch => folderMatch.matches().sort(searchMatchComparer) + .map(fileMatch => fileMatchToSearchResultFormat(fileMatch, labelFormatter))))); + + return { matchRanges: allResults.matchRanges.map(translateRangeLines(header.length)), text: header.concat(allResults.text.length ? allResults.text : ['No Results']) }; +}; + +export const openNewSearchEditor = + async (editorService: IEditorService, instantiationService: IInstantiationService) => { + const activeEditor = editorService.activeTextEditorWidget; + let activeModel: ICodeEditor | undefined; + if (isDiffEditor(activeEditor)) { + if (activeEditor.getOriginalEditor().hasTextFocus()) { + activeModel = activeEditor.getOriginalEditor(); + } else { + activeModel = activeEditor.getModifiedEditor(); + } + } else { + activeModel = activeEditor as ICodeEditor | undefined; + } + const selection = activeModel?.getSelection(); + let selected = (selection && activeModel?.getModel()?.getValueInRange(selection)) ?? ''; + await editorService.openEditor(instantiationService.createInstance(SearchEditorInput, { query: selected }, undefined, undefined), { pinned: true }); + }; + +export const createEditorFromSearchResult = + async (searchResult: SearchResult, rawIncludePattern: string, rawExcludePattern: string, labelService: ILabelService, editorService: IEditorService, instantiationService: IInstantiationService) => { + if (!searchResult.query) { + console.error('Expected searchResult.query to be defined. Got', searchResult); + return; + } + + const searchTerm = searchResult.query.contentPattern.pattern.replace(/[^\w-_. ]/g, '') || 'Search'; + + const labelFormatter = (uri: URI): string => labelService.getUriLabel(uri, { relative: true }); + + const results = serializeSearchResultForEditor(searchResult, rawIncludePattern, rawExcludePattern, 0, labelFormatter, true); + const contents = results.text.join(lineDelimiter); + let possible = { + contents, + mode: 'search-result', + resource: URI.from({ scheme: network.Schemas.untitled, path: searchTerm }) + }; + + let id = 0; + + let existing = editorService.getOpened(possible); + while (existing) { + if (existing instanceof UntitledTextEditorInput) { + const model = await existing.resolve(); + const existingContents = model.textEditorModel.getValue(EndOfLinePreference.LF); + if (existingContents === contents) { + break; + } + } + possible.resource = possible.resource.with({ path: searchTerm + '-' + ++id }); + existing = editorService.getOpened(possible); + } + + const input = instantiationService.createInstance( + SearchEditorInput, + { + query: searchResult.query.contentPattern.pattern, + regexp: !!searchResult.query.contentPattern.isRegExp, + caseSensitive: !!searchResult.query.contentPattern.isCaseSensitive, + wholeWord: !!searchResult.query.contentPattern.isWordMatch, + includes: rawIncludePattern, + excludes: rawExcludePattern, + contextLines: 0, + useIgnores: !searchResult.query.userDisabledExcludesAndIgnoreFiles, + showIncludesExcludes: !!(rawExcludePattern || rawExcludePattern || searchResult.query.userDisabledExcludesAndIgnoreFiles) + }, contents, undefined); + + const editor = await editorService.openEditor(input, { pinned: true }) as SearchEditor; + const model = assertIsDefined(editor.getModel()); + model.deltaDecorations([], results.matchRanges.map(range => ({ range, options: { className: 'searchEditorFindMatch', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } }))); + }; + +registerThemingParticipant((theme, collector) => { + collector.addRule(`.monaco-editor .searchEditorFindMatch { background-color: ${theme.getColor(searchEditorFindMatch)}; }`); + + const findMatchHighlightBorder = theme.getColor(searchEditorFindMatchBorder); + if (findMatchHighlightBorder) { + collector.addRule(`.monaco-editor .searchEditorFindMatch { border: 1px ${theme.type === 'hc' ? 'dotted' : 'solid'} ${findMatchHighlightBorder}; box-sizing: border-box; }`); + } +}); diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 9b0140d322..88fcb2813a 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 { TreeResourceNavigator2, 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, VIEWLET_ID, SearchSortOrder } from 'vs/workbench/services/search/common/search'; +import { IPatternInfo, ISearchComplete, ISearchConfiguration, ISearchConfigurationProperties, ITextQuery, VIEW_ID, VIEWLET_ID, SearchSortOrder, PANEL_ID } 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'; @@ -53,7 +53,7 @@ import { getOutOfWorkspaceEditorResources } from 'vs/workbench/contrib/search/co import { FileMatch, FileMatchOrMatch, IChangeEvent, ISearchWorkbenchService, Match, RenderableMatch, searchMatchComparer, SearchModel, SearchResult, FolderMatch, FolderMatchWithResource } from 'vs/workbench/contrib/search/common/searchModel'; import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IPreferencesService, ISettingsEditorOptions } from 'vs/workbench/services/preferences/common/preferences'; -import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { relativePath } from 'vs/base/common/resources'; import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; @@ -64,7 +64,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { MultiCursorSelectionController } from 'vs/editor/contrib/multicursor/multicursor'; import { Selection } from 'vs/editor/common/core/selection'; import { SIDE_BAR_BACKGROUND, PANEL_BACKGROUND } from 'vs/workbench/common/theme'; -import { createEditorFromSearchResult } from 'vs/workbench/contrib/search/browser/searchEditor'; +import { createEditorFromSearchResult } from 'vs/workbench/contrib/search/browser/searchEditorCommands'; import { ILabelService } from 'vs/platform/label/common/label'; import { Color, RGBA } from 'vs/base/common/color'; @@ -156,13 +156,13 @@ export class SearchView extends ViewPane { @INotificationService private readonly notificationService: INotificationService, @IDialogService private readonly dialogService: IDialogService, @IContextViewService private readonly contextViewService: IContextViewService, - @IInstantiationService private readonly instantiationService: IInstantiationService, + @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @ISearchWorkbenchService private readonly searchWorkbenchService: ISearchWorkbenchService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IReplaceService private readonly replaceService: IReplaceService, - @IUntitledTextEditorService private readonly untitledTextEditorService: IUntitledTextEditorService, + @ITextFileService private readonly textFileService: ITextFileService, @IPreferencesService private readonly preferencesService: IPreferencesService, @IThemeService protected themeService: IThemeService, @ISearchHistoryService private readonly searchHistoryService: ISearchHistoryService, @@ -174,7 +174,7 @@ export class SearchView extends ViewPane { @ILabelService private readonly labelService: ILabelService, @IOpenerService private readonly openerService: IOpenerService ) { - super({ ...options, id: VIEW_ID, ariaHeaderLabel: nls.localize('searchView', "Search") }, keybindingService, contextMenuService, configurationService, contextKeyService); + super({ ...options, id: VIEW_ID, ariaHeaderLabel: nls.localize('searchView', "Search") }, keybindingService, contextMenuService, configurationService, contextKeyService, instantiationService); this.viewletVisible = Constants.SearchViewVisibleKey.bindTo(contextKeyService); this.viewletFocused = Constants.SearchViewFocusedKey.bindTo(contextKeyService); @@ -214,7 +214,7 @@ export class SearchView extends ViewPane { this.viewletState = this.memento.getMemento(StorageScope.WORKSPACE); this._register(this.fileService.onFileChanges(e => this.onFilesChanged(e))); - this._register(this.untitledTextEditorService.onDidDisposeModel(e => this.onUntitledDidDispose(e))); + this._register(this.textFileService.untitled.onDidDisposeModel(e => this.onUntitledDidDispose(e))); this._register(this.contextService.onDidChangeWorkbenchState(() => this.onDidChangeWorkbenchState())); this._register(this.searchHistoryService.onDidClearHistory(() => this.clearHistory())); @@ -595,7 +595,7 @@ export class SearchView extends ViewPane { let progressComplete: () => void; let progressReporter: IProgress; - this.progressService.withProgress({ location: VIEWLET_ID, delay: 100, total: occurrences }, p => { + this.progressService.withProgress({ location: this.position === SearchViewPosition.SideBar ? VIEWLET_ID : PANEL_ID, delay: 100, total: occurrences }, p => { progressReporter = p; return new Promise(resolve => progressComplete = resolve); @@ -879,7 +879,7 @@ export class SearchView extends ViewPane { } this.searchWidget.setValue(selectedText, true); updatedText = true; - this.onQueryChanged(false); + if (this.searchConfig.searchOnType) { this.onQueryChanged(false); } } } @@ -1347,7 +1347,7 @@ export class SearchView extends ViewPane { private doSearch(query: ITextQuery, options: ITextQueryBuilderOptions, excludePatternText: string, includePatternText: string, triggeredOnType: boolean): Thenable { let progressComplete: () => void; - this.progressService.withProgress({ location: VIEWLET_ID, delay: triggeredOnType ? 300 : 0 }, _progress => { + this.progressService.withProgress({ location: this.position === SearchViewPosition.SideBar ? VIEWLET_ID : PANEL_ID, delay: triggeredOnType ? 300 : 0 }, _progress => { return new Promise(resolve => progressComplete = resolve); }); @@ -1515,13 +1515,13 @@ export class SearchView extends ViewPane { event.stopPropagation(); } })); - } + }; private onOpenSettings = (e: dom.EventLike): void => { dom.EventHelper.stop(e, false); this.openSettings('.exclude'); - } + }; private openSettings(query: string): Promise { const options: ISettingsEditorOptions = { query }; @@ -1534,7 +1534,7 @@ export class SearchView extends ViewPane { dom.EventHelper.stop(e, false); this.openerService.open(URI.parse('https://go.microsoft.com/fwlink/?linkid=853977')); - } + }; private updateSearchResultCount(disregardExcludesAndIgnores?: boolean): void { const fileCount = this.viewModel.searchResult.fileCount(); @@ -1559,7 +1559,7 @@ export class SearchView extends ViewPane { this.messageDisposables.push(dom.addDisposableListener(openInEditorLink, dom.EventType.CLICK, (e: MouseEvent) => { dom.EventHelper.stop(e, false); - createEditorFromSearchResult(this.searchResult, this.searchIncludePattern.getValue(), this.searchExcludePattern.getValue(), this.labelService, this.editorService); + createEditorFromSearchResult(this.searchResult, this.searchIncludePattern.getValue(), this.searchExcludePattern.getValue(), this.labelService, this.editorService, this.instantiationService); })); } else { @@ -1812,6 +1812,8 @@ export class SearchView extends ViewPane { this.searchHistoryService.save(history); + this.memento.saveMemento(); + super.saveState(); } diff --git a/src/vs/workbench/contrib/search/browser/searchWidget.ts b/src/vs/workbench/contrib/search/browser/searchWidget.ts index 802ec06910..a54cef2975 100644 --- a/src/vs/workbench/contrib/search/browser/searchWidget.ts +++ b/src/vs/workbench/contrib/search/browser/searchWidget.ts @@ -9,7 +9,7 @@ import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { Button, IButtonOptions } from 'vs/base/browser/ui/button/button'; import { FindInput, IFindInputOptions } from 'vs/base/browser/ui/findinput/findInput'; import { ReplaceInput } from 'vs/base/browser/ui/findinput/replaceInput'; -import { IMessage } from 'vs/base/browser/ui/inputbox/inputBox'; +import { IMessage, InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { Widget } from 'vs/base/browser/ui/widget'; import { Action } from 'vs/base/common/actions'; import { Delayer } from 'vs/base/common/async'; @@ -24,7 +24,7 @@ import { IContextViewService } from 'vs/platform/contextview/browser/contextView import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search'; -import { attachFindReplaceInputBoxStyler } from 'vs/platform/theme/common/styler'; +import { attachFindReplaceInputBoxStyler, attachInputBoxStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ContextScopedFindInput, ContextScopedReplaceInput } from 'vs/platform/browser/contextScopedHistoryWidget'; import { appendKeyBindingLabel, isSearchViewFocused } from 'vs/workbench/contrib/search/browser/searchActions'; @@ -34,6 +34,7 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; import { isMacintosh } from 'vs/base/common/platform'; +import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox'; /** Specified in searchview.css */ export const SingleLineInputHeight = 24; @@ -47,6 +48,8 @@ export interface ISearchWidgetOptions { searchHistory?: string[]; replaceHistory?: string[]; preserveCase?: boolean; + _hideReplaceToggle?: boolean; // TODO: Search Editor's replace experience + showContextToggle?: boolean; } class ReplaceAllAction extends Action { @@ -103,7 +106,7 @@ export class SearchWidget extends Widget { private static readonly REPLACE_ALL_ENABLED_LABEL = (keyBindingService2: IKeybindingService): string => { const kb = keyBindingService2.lookupKeybinding(ReplaceAllAction.ID); return appendKeyBindingLabel(nls.localize('search.action.replaceAll.enabled.label', "Replace All"), kb, keyBindingService2); - } + }; domNode!: HTMLElement; @@ -151,7 +154,12 @@ export class SearchWidget extends Widget { private _onDidHeightChange = this._register(new Emitter()); readonly onDidHeightChange: Event = this._onDidHeightChange.event; + private readonly _onDidToggleContext = new Emitter(); + readonly onDidToggleContext: Event = this._onDidToggleContext.event; + private temporarilySkipSearchOnChange = false; + private showContextCheckbox!: Checkbox; + private contextLinesInput!: InputBox; constructor( container: HTMLElement, @@ -168,7 +176,9 @@ export class SearchWidget extends Widget { this.replaceActive = Constants.ReplaceActiveKey.bindTo(this.contextKeyService); this.searchInputBoxFocused = Constants.SearchInputBoxFocusedKey.bindTo(this.contextKeyService); this.replaceInputBoxFocused = Constants.ReplaceInputBoxFocusedKey.bindTo(this.contextKeyService); + this._replaceHistoryDelayer = new Delayer(500); + this._searchDelayer = this._register(new Delayer(this.searchConfiguration.searchOnTypeDebouncePeriod)); this.render(container, options); @@ -275,7 +285,9 @@ export class SearchWidget extends Widget { this.domNode = dom.append(container, dom.$('.search-widget')); this.domNode.style.position = 'relative'; - this.renderToggleReplaceButton(this.domNode); + if (!options._hideReplaceToggle) { + this.renderToggleReplaceButton(this.domNode); + } this.renderSearchInput(this.domNode, options); this.renderReplaceInput(this.domNode, options); @@ -356,6 +368,41 @@ export class SearchWidget extends Widget { this.ignoreGlobalFindBufferOnNextFocus = false; })); this._register(this.searchInputFocusTracker.onDidBlur(() => this.searchInputBoxFocused.set(false))); + + + this.showContextCheckbox = new Checkbox({ isChecked: false, title: nls.localize('showContext', "Show Context"), actionClassName: 'codicon-list-selection' }); + this._register(this.showContextCheckbox.onChange(() => this.onContextLinesChanged())); + + if (options.showContextToggle) { + this.contextLinesInput = new InputBox(searchInputContainer, this.contextViewService, { type: 'number' }); + dom.addClass(this.contextLinesInput.element, 'context-lines-input'); + this.contextLinesInput.value = '2'; + this._register(this.contextLinesInput.onDidChange(() => this.onContextLinesChanged())); + this._register(attachInputBoxStyler(this.contextLinesInput, this.themeService)); + dom.append(searchInputContainer, this.showContextCheckbox.domNode); + } + } + + private onContextLinesChanged() { + dom.toggleClass(this.domNode, 'show-context', this.showContextCheckbox.checked); + this._onDidToggleContext.fire(); + + if (this.contextLinesInput.value.includes('-')) { + this.contextLinesInput.value = '0'; + } + + this._onDidToggleContext.fire(); + } + + public setContextLines(lines: number) { + if (!this.contextLinesInput) { return; } + if (lines === 0) { + this.showContextCheckbox.checked = false; + } else { + this.showContextCheckbox.checked = true; + this.contextLinesInput.value = '' + lines; + } + dom.toggleClass(this.domNode, 'show-context', this.showContextCheckbox.checked); } private renderReplaceInput(parent: HTMLElement, options: ISearchWidgetOptions): void { @@ -409,8 +456,8 @@ export class SearchWidget extends Widget { } setValue(value: string, skipSearchOnChange: boolean) { - this.searchInput.setValue(value); this.temporarilySkipSearchOnChange = skipSearchOnChange || this.temporarilySkipSearchOnChange; + this.searchInput.setValue(value); } setReplaceAllActionState(enabled: boolean): void { @@ -439,7 +486,6 @@ export class SearchWidget extends Widget { return null; } try { - // tslint:disable-next-line: no-unused-expression new RegExp(value, 'u'); } catch (e) { return { content: e.message }; @@ -457,7 +503,31 @@ export class SearchWidget extends Widget { this.temporarilySkipSearchOnChange = false; } else { this._onSearchCancel.fire({ focus: false }); - this._searchDelayer.trigger((() => this.submitSearch(true)), this.searchConfiguration.searchOnTypeDebouncePeriod); + if (this.searchInput.getRegex()) { + try { + const regex = new RegExp(this.searchInput.getValue(), 'ug'); + const matchienessHeuristic = ` + ~!@#$%^&*()_+ + \`1234567890-= + qwertyuiop[]\\ + QWERTYUIOP{}| + asdfghjkl;' + ASDFGHJKL:" + zxcvbnm,./ + ZXCVBNM<>? `.match(regex)?.length ?? 0; + + const delayMultiplier = + matchienessHeuristic < 50 ? 1 : + matchienessHeuristic < 100 ? 5 : // expressions like `.` or `\w` + 10; // only things matching empty string + + this._searchDelayer.trigger((() => this.submitSearch(true)), this.searchConfiguration.searchOnTypeDebouncePeriod * delayMultiplier); + } catch { + // pass + } + } else { + this._searchDelayer.trigger((() => this.submitSearch(true)), this.searchConfiguration.searchOnTypeDebouncePeriod); + } } } } @@ -581,6 +651,15 @@ export class SearchWidget extends Widget { this._onSearchSubmit.fire(triggeredOnType); } + contextLines() { + return this.showContextCheckbox.checked ? +this.contextLinesInput.value : 0; + } + + toggleContextLines() { + this.showContextCheckbox.checked = !this.showContextCheckbox.checked; + this.onContextLinesChanged(); + } + dispose(): void { this.setReplaceAllActionState(false); super.dispose(); diff --git a/src/vs/workbench/contrib/search/common/constants.ts b/src/vs/workbench/contrib/search/common/constants.ts index ae4dcfd2fc..e484730962 100644 --- a/src/vs/workbench/contrib/search/common/constants.ts +++ b/src/vs/workbench/contrib/search/common/constants.ts @@ -16,8 +16,7 @@ export const CopyPathCommandId = 'search.action.copyPath'; export const CopyMatchCommandId = 'search.action.copyMatch'; export const CopyAllCommandId = 'search.action.copyAll'; export const OpenInEditorCommandId = 'search.action.openInEditor'; -export const RerunEditorSearchCommandId = 'search.action.rerunEditorSearch'; -export const RerunEditorSearchWithContextCommandId = 'search.action.rerunEditorSearchWithContext'; +export const OpenNewEditorCommandId = 'search.action.openNewEditor'; export const ClearSearchHistoryCommandId = 'search.action.clearHistory'; export const FocusSearchListCommandID = 'search.action.focusSearchList'; export const ReplaceActionId = 'search.action.replace'; @@ -27,6 +26,10 @@ export const CloseReplaceWidgetActionId = 'closeReplaceInFilesWidget'; export const ToggleCaseSensitiveCommandId = 'toggleSearchCaseSensitive'; export const ToggleWholeWordCommandId = 'toggleSearchWholeWord'; export const ToggleRegexCommandId = 'toggleSearchRegex'; +export const ToggleSearchEditorCaseSensitiveCommandId = 'toggleSearchEditorCaseSensitive'; +export const ToggleSearchEditorWholeWordCommandId = 'toggleSearchEditorWholeWord'; +export const ToggleSearchEditorRegexCommandId = 'toggleSearchEditorRegex'; +export const ToggleSearchEditorContextLinesCommandId = 'toggleSearchEditorContextLines'; export const AddCursorsAtSearchResults = 'addCursorsAtSearchResults'; export const RevealInSideBarForSearchResults = 'search.action.revealInSideBar'; @@ -42,6 +45,7 @@ export const PatternExcludesFocusedKey = new RawContextKey('patternExcl export const ReplaceActiveKey = new RawContextKey('replaceActive', false); export const HasSearchResults = new RawContextKey('hasSearchResult', false); export const EnableSearchEditorPreview = new RawContextKey('previewSearchEditor', false); +export const InSearchEditor = new RawContextKey('inSearchEditor', false); export const FirstMatchFocusKey = new RawContextKey('firstMatchFocus', false); export const FileMatchOrMatchFocusKey = new RawContextKey('fileMatchOrMatchFocus', false); // This is actually, Match or File or Folder diff --git a/src/vs/workbench/contrib/search/common/queryBuilder.ts b/src/vs/workbench/contrib/search/common/queryBuilder.ts index 534d110295..149605d092 100644 --- a/src/vs/workbench/contrib/search/common/queryBuilder.ts +++ b/src/vs/workbench/contrib/search/common/queryBuilder.ts @@ -151,16 +151,18 @@ export class QueryBuilder { private commonQuery(folderResources: uri[] = [], options: ICommonQueryBuilderOptions = {}): ICommonQueryProps { let includeSearchPathsInfo: ISearchPathsInfo = {}; if (options.includePattern) { + const includePattern = normalizeSlashes(options.includePattern); includeSearchPathsInfo = options.expandPatterns ? - this.parseSearchPaths(options.includePattern) : - { pattern: patternListToIExpression(options.includePattern) }; + this.parseSearchPaths(includePattern) : + { pattern: patternListToIExpression(includePattern) }; } let excludeSearchPathsInfo: ISearchPathsInfo = {}; if (options.excludePattern) { + const excludePattern = normalizeSlashes(options.excludePattern); excludeSearchPathsInfo = options.expandPatterns ? - this.parseSearchPaths(options.excludePattern) : - { pattern: patternListToIExpression(options.excludePattern) }; + this.parseSearchPaths(excludePattern) : + { pattern: patternListToIExpression(excludePattern) }; } // Build folderQueries from searchPaths, if given, otherwise folderResources diff --git a/src/vs/workbench/contrib/search/common/searchModel.ts b/src/vs/workbench/contrib/search/common/searchModel.ts index f1f6700ce7..f8f973fd45 100644 --- a/src/vs/workbench/contrib/search/common/searchModel.ts +++ b/src/vs/workbench/contrib/search/common/searchModel.ts @@ -543,15 +543,13 @@ export class FolderMatch extends Disposable { replace(match: FileMatch): Promise { return this.replaceService.replace([match]).then(() => { - this.doRemove(match, false, true); + this.doRemove(match); }); } replaceAll(): Promise { const matches = this.matches(); - return this.replaceService.replace(matches).then(() => { - matches.forEach(match => this.doRemove(match, false, true)); - }); + return this.replaceService.replace(matches).then(() => this.doRemove(matches)); } matches(): FileMatch[] { diff --git a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts index 28a06cd940..5624fb524b 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts @@ -146,7 +146,7 @@ export class SnippetCompletionProvider implements CompletionItemProvider { i = to; } } - return { suggestions }; + return { suggestions, isDetailsResolved: true }; }); } diff --git a/src/vs/workbench/contrib/snippets/browser/tabCompletion.ts b/src/vs/workbench/contrib/snippets/browser/tabCompletion.ts index e8f65e7b32..7333c7e110 100644 --- a/src/vs/workbench/contrib/snippets/browser/tabCompletion.ts +++ b/src/vs/workbench/contrib/snippets/browser/tabCompletion.ts @@ -10,7 +10,7 @@ import { ISnippetsService } from './snippets.contribution'; import { getNonWhitespacePrefix } from './snippetsService'; import { endsWith } from 'vs/base/common/strings'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import * as editorCommon from 'vs/editor/common/editorCommon'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { Range } from 'vs/editor/common/core/range'; import { registerEditorContribution, EditorCommand, registerEditorCommand } from 'vs/editor/browser/editorExtensions'; import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; @@ -21,7 +21,7 @@ import { Snippet } from './snippetsFile'; import { SnippetCompletion } from './snippetCompletionProvider'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; -export class TabCompletionController implements editorCommon.IEditorContribution { +export class TabCompletionController implements IEditorContribution { public static readonly ID = 'editor.tabCompletionController'; static readonly ContextKey = new RawContextKey('hasSnippetCompletions', undefined); diff --git a/src/vs/workbench/contrib/surveys/browser/languageSurveys.contribution.ts b/src/vs/workbench/contrib/surveys/browser/languageSurveys.contribution.ts index 191e0e2777..41060b14a0 100644 --- a/src/vs/workbench/contrib/surveys/browser/languageSurveys.contribution.ts +++ b/src/vs/workbench/contrib/surveys/browser/languageSurveys.contribution.ts @@ -13,12 +13,14 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag import { ISurveyData, IProductService } from 'vs/platform/product/common/productService'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { Severity, INotificationService } from 'vs/platform/notification/common/notification'; -import { ITextFileService, StateChange } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileService, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { URI } from 'vs/base/common/uri'; import { platform } from 'vs/base/common/process'; +import { RunOnceWorker } from 'vs/base/common/async'; +import { Disposable } from 'vs/base/common/lifecycle'; -class LanguageSurvey { +class LanguageSurvey extends Disposable { constructor( data: ISurveyData, @@ -30,6 +32,8 @@ class LanguageSurvey { openerService: IOpenerService, productService: IProductService ) { + super(); + const SESSION_COUNT_KEY = `${data.surveyId}.sessionCount`; const LAST_SESSION_DATE_KEY = `${data.surveyId}.lastSessionDate`; const SKIP_VERSION_KEY = `${data.surveyId}.skipVersion`; @@ -45,18 +49,20 @@ class LanguageSurvey { const date = new Date().toDateString(); if (storageService.getNumber(EDITED_LANGUAGE_COUNT_KEY, StorageScope.GLOBAL, 0) < data.editCount) { - textFileService.models.onModelsSaved(e => { - e.forEach(event => { - if (event.kind === StateChange.SAVED) { - const model = modelService.getModel(event.resource); - if (model && model.getModeId() === data.languageId && date !== storageService.get(EDITED_LANGUAGE_DATE_KEY, StorageScope.GLOBAL)) { - const editedCount = storageService.getNumber(EDITED_LANGUAGE_COUNT_KEY, StorageScope.GLOBAL, 0) + 1; - storageService.store(EDITED_LANGUAGE_COUNT_KEY, editedCount, StorageScope.GLOBAL); - storageService.store(EDITED_LANGUAGE_DATE_KEY, date, StorageScope.GLOBAL); - } + + // Process model-save event every 250ms to reduce load + const onModelsSavedWorker = this._register(new RunOnceWorker(models => { + models.forEach(m => { + const model = modelService.getModel(m.resource); + if (model && model.getModeId() === data.languageId && date !== storageService.get(EDITED_LANGUAGE_DATE_KEY, StorageScope.GLOBAL)) { + const editedCount = storageService.getNumber(EDITED_LANGUAGE_COUNT_KEY, StorageScope.GLOBAL, 0) + 1; + storageService.store(EDITED_LANGUAGE_COUNT_KEY, editedCount, StorageScope.GLOBAL); + storageService.store(EDITED_LANGUAGE_DATE_KEY, date, StorageScope.GLOBAL); } }); - }); + }, 250)); + + this._register(textFileService.files.onDidSave(e => onModelsSavedWorker.work(e.model))); } const lastSessionDate = storageService.get(LAST_SESSION_DATE_KEY, StorageScope.GLOBAL, new Date(0).toDateString()); diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index c4564eb4cd..6082e3b09e 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -70,7 +70,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c import { RunAutomaticTasks } from 'vs/workbench/contrib/tasks/browser/runAutomaticTasks'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService'; import { format } from 'vs/base/common/jsonFormatter'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { applyEdits } from 'vs/base/common/jsonEdit'; @@ -256,7 +256,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @ITerminalInstanceService private readonly terminalInstanceService: ITerminalInstanceService, - @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, + @IRemotePathService private readonly remotePathService: IRemotePathService, @ITextModelService private readonly textModelResolverService: ITextModelService, @IPreferencesService private readonly preferencesService: IPreferencesService ) { @@ -1316,7 +1316,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer this.modelService, this.configurationResolverService, this.telemetryService, this.contextService, this.environmentService, AbstractTaskService.OutputChannelId, this.fileService, this.terminalInstanceService, - this.remoteAgentService, + this.remotePathService, (workspaceFolder: IWorkspaceFolder) => { if (!workspaceFolder) { return undefined; diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index d2f54bf5d2..4473c208c9 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -43,7 +43,7 @@ import { URI } from 'vs/base/common/uri'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { Schemas } from 'vs/base/common/network'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; -import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService'; import { env as processEnv, cwd as processCwd } from 'vs/base/common/process'; interface TerminalData { @@ -176,7 +176,7 @@ export class TerminalTaskSystem implements ITaskSystem { private outputChannelId: string, private fileService: IFileService, private terminalInstanceService: ITerminalInstanceService, - private remoteAgentService: IRemoteAgentService, + private remotePathService: IRemotePathService, taskSystemInfoResolver: TaskSystemInfoResolver, ) { @@ -829,14 +829,6 @@ export class TerminalTaskSystem implements ITaskSystem { return nls.localize('TerminalTaskSystem.terminalName', 'Task - {0}', needsFolderQualification ? task.getQualifiedLabel() : task.configurationProperties.name); } - private async getUserHome(): Promise { - const env = await this.remoteAgentService.getEnvironment(); - if (env) { - return env.userHome; - } - return URI.from({ scheme: Schemas.file, path: this.environmentService.userHome }); - } - private async createShellLaunchConfig(task: CustomTask | ContributedTask, workspaceFolder: IWorkspaceFolder | undefined, variableResolver: VariableResolver, platform: Platform.Platform, options: CommandOptions, command: CommandString, args: CommandString[], waitOnExit: boolean | string): Promise { let shellLaunchConfig: IShellLaunchConfig; let isShellCommand = task.command.runtime === RuntimeType.Shell; @@ -867,7 +859,7 @@ export class TerminalTaskSystem implements ITaskSystem { windowsShellArgs = true; let basename = path.basename(shellLaunchConfig.executable!).toLowerCase(); // If we don't have a cwd, then the terminal uses the home dir. - const userHome = await this.getUserHome(); + const userHome = await this.remotePathService.userHome; if (basename === 'cmd.exe' && ((options.cwd && isUNC(options.cwd)) || (!options.cwd && isUNC(userHome.fsPath)))) { return undefined; } diff --git a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts index 45be818320..e5fe9ca9cc 100644 --- a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts +++ b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts @@ -19,20 +19,46 @@ import ErrorTelemetry from 'vs/platform/telemetry/browser/errorTelemetry'; import { configurationTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; +import { ITextFileService, ITextFileModelSaveEvent, ITextFileModelLoadEvent } from 'vs/workbench/services/textfile/common/textfiles'; +import { extname, basename, isEqual, isEqualOrParent, joinPath } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { Schemas } from 'vs/base/common/network'; +import { guessMimeTypes } from 'vs/base/common/mime'; +import { hash } from 'vs/base/common/hash'; + +type TelemetryData = { + mimeType: string; + ext: string; + path: number; + reason?: number; + whitelistedjson?: string; +}; + +type FileTelemetryDataFragment = { + mimeType: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + ext: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + path: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + reason?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; + whitelistedjson?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; +}; export class TelemetryContribution extends Disposable implements IWorkbenchContribution { + private static WHITELIST_JSON = ['package.json', 'package-lock.json', 'tsconfig.json', 'jsconfig.json', 'bower.json', '.eslintrc.json', 'tslint.json', 'composer.json']; + private static WHITELIST_WORKSPACE_JSON = ['settings.json', 'extensions.json', 'tasks.json', 'launch.json']; + constructor( - @ITelemetryService telemetryService: ITelemetryService, - @IWorkspaceContextService contextService: IWorkspaceContextService, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IActivityBarService activityBarService: IActivityBarService, @ILifecycleService lifecycleService: ILifecycleService, @IEditorService editorService: IEditorService, @IKeybindingService keybindingsService: IKeybindingService, @IWorkbenchThemeService themeService: IWorkbenchThemeService, - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IConfigurationService configurationService: IConfigurationService, - @IViewletService viewletService: IViewletService + @IViewletService viewletService: IViewletService, + @ITextFileService textFileService: ITextFileService, ) { super(); @@ -45,6 +71,7 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr outerHeight: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; outerWidth: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; }; + type WorkspaceLoadClassification = { userAgent: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; emptyWorkbench: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; @@ -59,6 +86,7 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr restoredEditors: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; startupKind: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; }; + type WorkspaceLoadEvent = { userAgent: string; windowSize: { innerHeight: number, innerWidth: number, outerHeight: number, outerWidth: number }; @@ -73,6 +101,7 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr restoredEditors: number; startupKind: StartupKind; }; + telemetryService.publicLog2('workspaceLoad', { userAgent: navigator.userAgent, windowSize: { innerHeight: window.innerHeight, innerWidth: window.innerWidth, outerHeight: window.outerHeight, outerWidth: window.outerWidth }, @@ -94,9 +123,94 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr // Configuration Telemetry this._register(configurationTelemetry(telemetryService, configurationService)); + // Files Telemetry + this._register(textFileService.files.onDidLoad(e => this.onTextFileModelLoaded(e))); + this._register(textFileService.files.onDidSave(e => this.onTextFileModelSaved(e))); + // Lifecycle this._register(lifecycleService.onShutdown(() => this.dispose())); } + + private onTextFileModelLoaded(e: ITextFileModelLoadEvent): void { + const settingsType = this.getTypeIfSettings(e.model.resource); + if (settingsType) { + type SettingsReadClassification = { + settingsType: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + }; + + this.telemetryService.publicLog2<{ settingsType: string }, SettingsReadClassification>('settingsRead', { settingsType }); // Do not log read to user settings.json and .vscode folder as a fileGet event as it ruins our JSON usage data + } else { + type FileGetClassification = {} & FileTelemetryDataFragment; + + this.telemetryService.publicLog2('fileGet', this.getTelemetryData(e.model.resource, e.reason)); + } + } + + private onTextFileModelSaved(e: ITextFileModelSaveEvent): void { + const settingsType = this.getTypeIfSettings(e.model.resource); + if (settingsType) { + type SettingsWrittenClassification = { + settingsType: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + }; + this.telemetryService.publicLog2<{ settingsType: string }, SettingsWrittenClassification>('settingsWritten', { settingsType }); // Do not log write to user settings.json and .vscode folder as a filePUT event as it ruins our JSON usage data + } else { + type FilePutClassfication = {} & FileTelemetryDataFragment; + this.telemetryService.publicLog2('filePUT', this.getTelemetryData(e.model.resource, e.reason)); + } + } + + private getTypeIfSettings(resource: URI): string { + if (extname(resource) !== '.json') { + return ''; + } + + // Check for global settings file + if (isEqual(resource, this.environmentService.settingsResource)) { + return 'global-settings'; + } + + // Check for keybindings file + if (isEqual(resource, this.environmentService.keybindingsResource)) { + return 'keybindings'; + } + + // Check for snippets + if (isEqualOrParent(resource, joinPath(this.environmentService.userRoamingDataHome, 'snippets'))) { + return 'snippets'; + } + + // Check for workspace settings file + const folders = this.contextService.getWorkspace().folders; + for (const folder of folders) { + if (isEqualOrParent(resource, folder.toResource('.azuredatastudio'))) { // {{SQL CARBON EDIT}} + const filename = basename(resource); + if (TelemetryContribution.WHITELIST_WORKSPACE_JSON.indexOf(filename) > -1) { + return `.azuredatastudio/${filename}`; // {{SQL CARBON EDIT}} + } + } + } + + return ''; + } + + private getTelemetryData(resource: URI, reason?: number): TelemetryData { + const ext = extname(resource); + const fileName = basename(resource); + const path = resource.scheme === Schemas.file ? resource.fsPath : resource.path; + const telemetryData = { + mimeType: guessMimeTypes(resource).join(', '), + ext, + path: hash(path), + reason, + whitelistedjson: undefined as string | undefined + }; + + if (ext === '.json' && TelemetryContribution.WHITELIST_JSON.indexOf(fileName) > -1) { + telemetryData['whitelistedjson'] = fileName; + } + + return telemetryData; + } } Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TelemetryContribution, LifecyclePhase.Restored); diff --git a/src/vs/workbench/contrib/terminal/browser/media/widgets.css b/src/vs/workbench/contrib/terminal/browser/media/widgets.css index 4bca7e3393..96223c2737 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/widgets.css +++ b/src/vs/workbench/contrib/terminal/browser/media/widgets.css @@ -8,15 +8,22 @@ left: 0; right: 0; bottom: 0; - pointer-events: none; } .monaco-workbench .terminal-message-widget { font-size: 12px; line-height: 19px; - padding: 4px 5px; + padding: 4px 8px; animation: fadein 100ms linear; white-space: nowrap; /* Must be drawn on the top of the terminal's canvases */ z-index: 20; -} \ No newline at end of file +} + +.monaco-workbench .terminal-message-widget p { + margin: 0px; +} + +.monaco-workbench .terminal-message-widget a { + color: #3794ff; +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index 790ea4d8cc..136774006c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -170,7 +170,17 @@ configurationRegistry.registerConfiguration({ default: DEFAULT_LINE_HEIGHT }, 'terminal.integrated.minimumContrastRatio': { - markdownDescription: nls.localize('terminal.integrated.minimumContrastRatio', "When set the foreground color of each cell will change to try meet the contrast ratio specified. Example values:\n\n- 1: The default, do nothing.\n- 4.5: Minimum for [WCAG AA compliance](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html).\n- 7: Minimum for [WCAG AAA compliance](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast7.html).\n- 21: White on black or black on white."), + markdownDescription: nls.localize('terminal.integrated.minimumContrastRatio', "When set the foreground color of each cell will change to try meet the contrast ratio specified. Example values:\n\n- 1: The default, do nothing.\n- 4.5: [WCAG AA compliance (minimum)](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html).\n- 7: [WCAG AAA compliance (enhanced)](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast7.html).\n- 21: White on black or black on white."), + type: 'number', + default: 1 + }, + 'terminal.integrated.fastScrollSensitivity': { + markdownDescription: nls.localize('terminal.integrated.fastScrollSensitivity', "Scrolling speed multiplier when pressing `Alt`."), + type: 'number', + default: 5 + }, + 'terminal.integrated.mouseWheelScrollSensitivity': { + markdownDescription: nls.localize('terminal.integrated.mouseWheelScrollSensitivity', "A multiplier to be used on the `deltaY` of mouse wheel scroll events."), type: 'number', default: 1 }, @@ -196,6 +206,11 @@ configurationRegistry.registerConfiguration({ enum: [TerminalCursorStyle.BLOCK, TerminalCursorStyle.LINE, TerminalCursorStyle.UNDERLINE], default: TerminalCursorStyle.BLOCK }, + 'terminal.integrated.cursorWidth': { + markdownDescription: nls.localize('terminal.integrated.cursorWidth', "Controls the width of the cursor when `#terminal.integrated.cursorStyle#` is set to `line`."), + type: 'number', + default: 1 + }, 'terminal.integrated.scrollback': { description: nls.localize('terminal.integrated.scrollback', "Controls the maximum amount of lines the terminal keeps in its buffer."), type: 'number', diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index f1cf682306..fa2247efd9 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -39,6 +39,7 @@ import { CommandTrackerAddon } from 'vs/workbench/contrib/terminal/browser/addon import { NavigationModeAddon } from 'vs/workbench/contrib/terminal/browser/addons/navigationModeAddon'; import { XTermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; // How long in milliseconds should an average frame take to render for a notification to appear // which suggests the fallback DOM-based renderer @@ -286,7 +287,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { @IConfigurationService private readonly _configurationService: IConfigurationService, @ILogService private readonly _logService: ILogService, @IStorageService private readonly _storageService: IStorageService, - @IAccessibilityService private readonly _accessibilityService: IAccessibilityService + @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, + @IOpenerService private readonly _openerService: IOpenerService ) { super(); @@ -478,7 +480,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { fastScrollSensitivity: editorOptions.fastScrollSensitivity, scrollSensitivity: editorOptions.mouseWheelScrollSensitivity, rendererType: config.rendererType === 'auto' || config.rendererType === 'experimentalWebgl' ? 'canvas' : config.rendererType, - wordSeparator: ' ()[]{}\',:;"`' + wordSeparator: ' ()[]{}\',"`' }); this._xterm = xterm; this._xtermCore = (xterm as any)._core as XTermCore; @@ -666,7 +668,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._refreshSelectionContextKey(); })); - const widgetManager = new TerminalWidgetManager(this._wrapperElement); + const widgetManager = new TerminalWidgetManager(this._wrapperElement, this._openerService); this._widgetManager = widgetManager; this._processManager.onProcessReady(() => this._linkHandler?.setWidgetManager(widgetManager)); @@ -700,7 +702,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Discard first frame time as it's normal to take longer frameTimes.shift(); - const medianTime = frameTimes.sort()[Math.floor(frameTimes.length / 2)]; + const medianTime = frameTimes.sort((a, b) => a - b)[Math.floor(frameTimes.length / 2)]; if (medianTime > SLOW_CANVAS_RENDER_THRESHOLD) { const promptChoices: IPromptChoice[] = [ { @@ -1227,10 +1229,13 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { const config = this._configHelper.config; this._setCursorBlink(config.cursorBlinking); this._setCursorStyle(config.cursorStyle); + this._setCursorWidth(config.cursorWidth); this._setCommandsToSkipShell(config.commandsToSkipShell); this._setEnableBell(config.enableBell); this._safeSetOption('scrollback', config.scrollback); this._safeSetOption('minimumContrastRatio', config.minimumContrastRatio); + this._safeSetOption('fastScrollSensitivity', config.fastScrollSensitivity); + this._safeSetOption('scrollSensitivity', config.mouseWheelScrollSensitivity); this._safeSetOption('macOptionIsMeta', config.macOptionIsMeta); this._safeSetOption('macOptionClickForcesSelection', config.macOptionClickForcesSelection); this._safeSetOption('rightClickSelectsWord', config.rightClickBehavior === 'selectWord'); @@ -1238,10 +1243,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Never set webgl as it's an addon not a rendererType this._safeSetOption('rendererType', config.rendererType === 'auto' ? 'canvas' : config.rendererType); } - - const editorOptions = this._configurationService.getValue('editor'); - this._safeSetOption('fastScrollSensitivity', editorOptions.fastScrollSensitivity); - this._safeSetOption('scrollSensitivity', editorOptions.mouseWheelScrollSensitivity); } public updateAccessibilitySupport(): void { @@ -1271,6 +1272,12 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } } + private _setCursorWidth(width: number): void { + if (this._xterm && this._xterm.getOption('cursorWidth') !== width) { + this._xterm.setOption('cursorWidth', width); + } + } + private _setCommandsToSkipShell(commands: string[]): void { const excludeCommands = commands.filter(command => command[0] === '-').map(command => command.slice(1)); this._skipTerminalCommands = DEFAULT_COMMANDS_TO_SKIP_SHELL.filter(defaultCommand => { @@ -1337,6 +1344,12 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._safeSetOption('fontWeight', config.fontWeight); this._safeSetOption('fontWeightBold', config.fontWeightBold); this._safeSetOption('drawBoldTextInBrightColors', config.drawBoldTextInBrightColors); + + // Any of the above setting changes could have changed the dimensions of the + // terminal, re-evaluate now. + this._initDimensions(); + cols = this.cols; + rows = this.rows; } if (isNaN(cols) || isNaN(rows)) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalLinkHandler.ts b/src/vs/workbench/contrib/terminal/browser/terminalLinkHandler.ts index 7dd1d01571..bf72072939 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalLinkHandler.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalLinkHandler.ts @@ -18,6 +18,7 @@ import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { posix, win32 } from 'vs/base/common/path'; import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { OperatingSystem, isMacintosh } from 'vs/base/common/platform'; +import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; const pathPrefix = '(\\.\\.?|\\~)'; const pathSeparatorClause = '\\/'; @@ -116,7 +117,7 @@ export class TerminalLinkHandler { const leftPosition = location.start.x * (charWidth! + (font.letterSpacing / window.devicePixelRatio)); const bottomPosition = offsetRow * (Math.ceil(charHeight! * window.devicePixelRatio) * font.lineHeight) / window.devicePixelRatio; - this._widgetManager.showMessage(leftPosition, bottomPosition, this._getLinkHoverString(), verticalAlignment); + this._widgetManager.showMessage(leftPosition, bottomPosition, this._getLinkHoverString(uri), verticalAlignment); } else { const target = (e.target as HTMLElement); const colWidth = target.offsetWidth / this._xterm.cols; @@ -124,7 +125,7 @@ export class TerminalLinkHandler { const leftPosition = location.start.x * colWidth; const bottomPosition = offsetRow * rowHeight; - this._widgetManager.showMessage(leftPosition, bottomPosition, this._getLinkHoverString(), verticalAlignment); + this._widgetManager.showMessage(leftPosition, bottomPosition, this._getLinkHoverString(uri), verticalAlignment); } }; this._leaveCallback = () => { @@ -277,19 +278,29 @@ export class TerminalLinkHandler { return isMacintosh ? event.metaKey : event.ctrlKey; } - private _getLinkHoverString(): string { + private _getLinkHoverString(uri: string): IMarkdownString { const editorConf = this._configurationService.getValue<{ multiCursorModifier: 'ctrlCmd' | 'alt' }>('editor'); + + let label = ''; if (editorConf.multiCursorModifier === 'ctrlCmd') { if (isMacintosh) { - return nls.localize('terminalLinkHandler.followLinkAlt.mac', "Option + click to follow link"); + label = nls.localize('terminalLinkHandler.followLinkAlt.mac', "Option + click"); } else { - return nls.localize('terminalLinkHandler.followLinkAlt', "Alt + click to follow link"); + label = nls.localize('terminalLinkHandler.followLinkAlt', "Alt + click"); + } + } else { + if (isMacintosh) { + label = nls.localize('terminalLinkHandler.followLinkCmd', "Cmd + click"); + } else { + label = nls.localize('terminalLinkHandler.followLinkCtrl', "Ctrl + click"); } } - if (isMacintosh) { - return nls.localize('terminalLinkHandler.followLinkCmd', "Cmd + click to follow link"); - } - return nls.localize('terminalLinkHandler.followLinkCtrl', "Ctrl + click to follow link"); + + const message: IMarkdownString = new MarkdownString(`[Follow Link](${uri}) (${label})`, true); + message.uris = { + [uri]: URI.parse(uri).toJSON() + }; + return message; } private get osPath(): IPath { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalPanel.ts b/src/vs/workbench/contrib/terminal/browser/terminalPanel.ts index f4046ada5a..8b6a43e5a4 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalPanel.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalPanel.ts @@ -88,7 +88,7 @@ export class TerminalPanel extends Panel { label: nls.localize('terminal.useMonospace', "Use 'monospace'"), run: () => this._configurationService.updateValue('terminal.integrated.fontFamily', 'monospace'), }]; - this._notificationService.prompt(Severity.Warning, nls.localize('terminal.monospaceOnly', "The terminal only supports monospace fonts."), choices); + this._notificationService.prompt(Severity.Warning, nls.localize('terminal.monospaceOnly', "The terminal only supports monospace fonts. Be sure to restart VS Code if this is a newly installed font."), choices); } } })); @@ -97,18 +97,14 @@ export class TerminalPanel extends Panel { this._register(this.onDidChangeVisibility(visible => { if (visible) { - if (this._terminalService.terminalInstances.length > 0) { - this._updateFont(); - this._updateTheme(); - } else { - // Check if instances were already restored as part of workbench restore - if (this._terminalService.terminalInstances.length === 0) { - this._terminalService.createTerminal(); - } - if (this._terminalService.terminalInstances.length > 0) { - this._updateFont(); - this._updateTheme(); - } + const hadTerminals = this._terminalService.terminalInstances.length > 0; + if (!hadTerminals) { + this._terminalService.createTerminal(); + } + this._updateFont(); + this._updateTheme(); + if (hadTerminals) { + this._terminalService.getActiveTab()?.setVisible(visible); } } })); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 21cd00663f..455999aa02 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -22,6 +22,7 @@ import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/ import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { Disposable } from 'vs/base/common/lifecycle'; +import { withNullAsUndefined } from 'vs/base/common/types'; /** The amount of time to consider terminal errors to be related to the launch */ const LAUNCHING_DURATION = 500; @@ -194,22 +195,22 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce ): Promise { const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(Schemas.file); const platformKey = platform.isWindows ? 'windows' : (platform.isMacintosh ? 'osx' : 'linux'); - const lastActiveWorkspace = activeWorkspaceRootUri ? this._workspaceContextService.getWorkspaceFolder(activeWorkspaceRootUri) : null; + const lastActiveWorkspace = activeWorkspaceRootUri ? withNullAsUndefined(this._workspaceContextService.getWorkspaceFolder(activeWorkspaceRootUri)) : undefined; if (!shellLaunchConfig.executable) { const defaultConfig = await this._terminalInstanceService.getDefaultShellAndArgs(false); shellLaunchConfig.executable = defaultConfig.shell; shellLaunchConfig.args = defaultConfig.args; } else { - shellLaunchConfig.executable = this._configurationResolverService.resolve(lastActiveWorkspace === null ? undefined : lastActiveWorkspace, shellLaunchConfig.executable); + shellLaunchConfig.executable = this._configurationResolverService.resolve(lastActiveWorkspace, shellLaunchConfig.executable); if (shellLaunchConfig.args) { if (Array.isArray(shellLaunchConfig.args)) { const resolvedArgs: string[] = []; for (const arg of shellLaunchConfig.args) { - resolvedArgs.push(this._configurationResolverService.resolve(lastActiveWorkspace === null ? undefined : lastActiveWorkspace, arg)); + resolvedArgs.push(this._configurationResolverService.resolve(lastActiveWorkspace, arg)); } shellLaunchConfig.args = resolvedArgs; } else { - shellLaunchConfig.args = this._configurationResolverService.resolve(lastActiveWorkspace === null ? undefined : lastActiveWorkspace, shellLaunchConfig.args); + shellLaunchConfig.args = this._configurationResolverService.resolve(lastActiveWorkspace, shellLaunchConfig.args); } } } @@ -217,7 +218,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce const initialCwd = terminalEnvironment.getCwd( shellLaunchConfig, this._environmentService.userHome, - lastActiveWorkspace ? lastActiveWorkspace : undefined, + lastActiveWorkspace, this._configurationResolverService, activeWorkspaceRootUri, this._configHelper.config.cwd, diff --git a/src/vs/workbench/contrib/terminal/browser/terminalWidgetManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalWidgetManager.ts index e31a851c34..8e923d68c1 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalWidgetManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalWidgetManager.ts @@ -4,6 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { IDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { URI } from 'vs/base/common/uri'; export enum WidgetVerticalAlignment { Bottom, @@ -20,7 +24,8 @@ export class TerminalWidgetManager implements IDisposable { private readonly _messageListeners = new DisposableStore(); constructor( - terminalWrapper: HTMLElement + terminalWrapper: HTMLElement, + private readonly _openerService: IOpenerService ) { this._container = document.createElement('div'); this._container.classList.add('terminal-widget-overlay'); @@ -48,20 +53,22 @@ export class TerminalWidgetManager implements IDisposable { mutationObserver.observe(this._xtermViewport, { attributes: true, attributeFilter: ['style'] }); } - public showMessage(left: number, y: number, text: string, verticalAlignment: WidgetVerticalAlignment = WidgetVerticalAlignment.Bottom): void { + public showMessage(left: number, y: number, text: IMarkdownString, verticalAlignment: WidgetVerticalAlignment = WidgetVerticalAlignment.Bottom): void { if (!this._container) { return; } dispose(this._messageWidget); this._messageListeners.clear(); - this._messageWidget = new MessageWidget(this._container, left, y, text, verticalAlignment); + this._messageWidget = new MessageWidget(this._container, left, y, text, verticalAlignment, this._openerService); } public closeMessage(): void { this._messageListeners.clear(); - if (this._messageWidget) { - this._messageListeners.add(MessageWidget.fadeOut(this._messageWidget)); - } + setTimeout(() => { + if (this._messageWidget && !this._messageWidget.mouseOver) { + this._messageListeners.add(MessageWidget.fadeOut(this._messageWidget)); + } + }, 50); } private _refreshHeight(): void { @@ -73,13 +80,16 @@ export class TerminalWidgetManager implements IDisposable { } class MessageWidget { - private _domNode: HTMLDivElement; + private _domNode: HTMLElement; + private _mouseOver = false; + private readonly _messageListeners = new DisposableStore(); public get left(): number { return this._left; } public get y(): number { return this._y; } - public get text(): string { return this._text; } + public get text(): IMarkdownString { return this._text; } public get domNode(): HTMLElement { return this._domNode; } public get verticalAlignment(): WidgetVerticalAlignment { return this._verticalAlignment; } + public get mouseOver(): boolean { return this._mouseOver; } public static fadeOut(messageWidget: MessageWidget): IDisposable { let handle: any; @@ -98,10 +108,16 @@ class MessageWidget { private _container: HTMLElement, private _left: number, private _y: number, - private _text: string, - private _verticalAlignment: WidgetVerticalAlignment + private _text: IMarkdownString, + private _verticalAlignment: WidgetVerticalAlignment, + private readonly _openerService: IOpenerService ) { - this._domNode = document.createElement('div'); + this._domNode = renderMarkdown(this._text, { + actionHandler: { + callback: this._handleLinkClicked.bind(this), + disposeables: this._messageListeners + } + }); this._domNode.style.position = 'absolute'; this._domNode.style.left = `${_left}px`; @@ -114,7 +130,15 @@ class MessageWidget { } this._domNode.classList.add('terminal-message-widget', 'fadeIn'); - this._domNode.textContent = _text; + this._domNode.addEventListener('mouseenter', () => { + this._mouseOver = true; + }); + + this._domNode.addEventListener('mouseleave', () => { + this._mouseOver = false; + this._messageListeners.add(MessageWidget.fadeOut(this)); + }); + this._container.appendChild(this._domNode); } @@ -122,5 +146,11 @@ class MessageWidget { if (this.domNode.parentElement === this._container) { this._container.removeChild(this.domNode); } + + this._messageListeners.dispose(); + } + + private _handleLinkClicked(content: string) { + this._openerService.open(URI.parse(content)); } } diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 35a4d55afc..eeb8ec4a9e 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -91,11 +91,14 @@ export interface ITerminalConfiguration { rightClickBehavior: 'default' | 'copyPaste' | 'paste' | 'selectWord'; cursorBlinking: boolean; cursorStyle: string; + cursorWidth: number; drawBoldTextInBrightColors: boolean; + fastScrollSensitivity: number; fontFamily: string; fontWeight: FontWeight; fontWeightBold: FontWeight; minimumContrastRatio: number; + mouseWheelScrollSensitivity: number; // fontLigatures: boolean; fontSize: number; letterSpacing: number; diff --git a/src/vs/workbench/contrib/terminal/common/terminalDataBuffering.ts b/src/vs/workbench/contrib/terminal/common/terminalDataBuffering.ts index 612c56407e..eca10bcc62 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalDataBuffering.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalDataBuffering.ts @@ -14,13 +14,16 @@ interface TerminalDataBuffer extends IDisposable { export class TerminalDataBufferer implements IDisposable { private readonly _terminalBufferMap = new Map(); + constructor(private readonly _callback: (id: number, data: string) => void) { + } + dispose() { for (const buffer of this._terminalBufferMap.values()) { buffer.dispose(); } } - startBuffering(id: number, event: Event, callback: (id: number, data: string) => void, throttleBy: number = 5): IDisposable { + startBuffering(id: number, event: Event, throttleBy: number = 5): IDisposable { let disposable: IDisposable; disposable = event((e: string) => { let buffer = this._terminalBufferMap.get(id); @@ -30,16 +33,13 @@ export class TerminalDataBufferer implements IDisposable { return; } - const timeoutId = setTimeout(() => { - this._terminalBufferMap.delete(id); - callback(id, buffer!.data.join('')); - }, throttleBy); + const timeoutId = setTimeout(() => this._flushBuffer(id), throttleBy); buffer = { data: [e], timeoutId: timeoutId, dispose: () => { clearTimeout(timeoutId); - this._terminalBufferMap.delete(id); + this._flushBuffer(id); disposable.dispose(); } }; @@ -54,4 +54,12 @@ export class TerminalDataBufferer implements IDisposable { buffer.dispose(); } } + + private _flushBuffer(id: number): void { + const buffer = this._terminalBufferMap.get(id); + if (buffer) { + this._terminalBufferMap.delete(id); + this._callback(id, buffer.data.join('')); + } + } } diff --git a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts index c20b2be958..30069563da 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts @@ -74,10 +74,10 @@ function mergeNonNullKeys(env: platform.IProcessEnvironment, other: ITerminalEnv } } -function resolveConfigurationVariables(configurationResolverService: IConfigurationResolverService, env: ITerminalEnvironment, lastActiveWorkspaceRoot: IWorkspaceFolder | null): ITerminalEnvironment { +function resolveConfigurationVariables(configurationResolverService: IConfigurationResolverService, env: ITerminalEnvironment, lastActiveWorkspaceRoot: IWorkspaceFolder | undefined): ITerminalEnvironment { Object.keys(env).forEach((key) => { const value = env[key]; - if (typeof value === 'string' && lastActiveWorkspaceRoot !== null) { + if (typeof value === 'string') { try { env[key] = configurationResolverService.resolve(lastActiveWorkspaceRoot, value); } catch (e) { @@ -346,7 +346,7 @@ function getShellSetting( export function createTerminalEnvironment( shellLaunchConfig: IShellLaunchConfig, - lastActiveWorkspace: IWorkspaceFolder | null, + lastActiveWorkspace: IWorkspaceFolder | undefined, envFromConfig: { userValue?: ITerminalEnvironment, value?: ITerminalEnvironment, defaultValue?: ITerminalEnvironment }, configurationResolverService: IConfigurationResolverService | undefined, isWorkspaceShellAllowed: boolean, diff --git a/src/vs/workbench/contrib/terminal/test/common/terminalDataBuffering.test.ts b/src/vs/workbench/contrib/terminal/test/common/terminalDataBuffering.test.ts index 7b12303c9a..6f7917dfdc 100644 --- a/src/vs/workbench/contrib/terminal/test/common/terminalDataBuffering.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/terminalDataBuffering.test.ts @@ -11,20 +11,28 @@ const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); suite('Workbench - TerminalDataBufferer', () => { let bufferer: TerminalDataBufferer; + let counter: { [id: number]: number }; + let data: { [id: number]: string }; setup(async () => { - bufferer = new TerminalDataBufferer(); + counter = {}; + data = {}; + bufferer = new TerminalDataBufferer((id, e) => { + if (!(id in counter)) { + counter[id] = 0; + } + counter[id]++; + if (!(id in data)) { + data[id] = ''; + } + data[id] = e; + }); }); test('start', async () => { - let terminalOnData = new Emitter(); - let counter = 0; - let data: string | undefined; + const terminalOnData = new Emitter(); - bufferer.startBuffering(1, terminalOnData.event, (id, e) => { - counter++; - data = e; - }, 0); + bufferer.startBuffering(1, terminalOnData.event, 0); terminalOnData.fire('1'); terminalOnData.fire('2'); @@ -34,33 +42,21 @@ suite('Workbench - TerminalDataBufferer', () => { terminalOnData.fire('4'); - assert.equal(counter, 1); - assert.equal(data, '123'); + assert.equal(counter[1], 1); + assert.equal(data[1], '123'); await wait(0); - assert.equal(counter, 2); - assert.equal(data, '4'); + assert.equal(counter[1], 2); + assert.equal(data[1], '4'); }); test('start 2', async () => { - let terminal1OnData = new Emitter(); - let terminal1Counter = 0; - let terminal1Data: string | undefined; + const terminal1OnData = new Emitter(); + const terminal2OnData = new Emitter(); - bufferer.startBuffering(1, terminal1OnData.event, (id, e) => { - terminal1Counter++; - terminal1Data = e; - }, 0); - - let terminal2OnData = new Emitter(); - let terminal2Counter = 0; - let terminal2Data: string | undefined; - - bufferer.startBuffering(2, terminal2OnData.event, (id, e) => { - terminal2Counter++; - terminal2Data = e; - }, 0); + bufferer.startBuffering(1, terminal1OnData.event, 0); + bufferer.startBuffering(2, terminal2OnData.event, 0); terminal1OnData.fire('1'); terminal2OnData.fire('4'); @@ -70,60 +66,41 @@ suite('Workbench - TerminalDataBufferer', () => { terminal2OnData.fire('6'); terminal2OnData.fire('7'); - assert.equal(terminal1Counter, 0); - assert.equal(terminal1Data, undefined); - assert.equal(terminal2Counter, 0); - assert.equal(terminal2Data, undefined); + assert.equal(counter[1], undefined); + assert.equal(data[1], undefined); + assert.equal(counter[2], undefined); + assert.equal(data[2], undefined); await wait(0); - assert.equal(terminal1Counter, 1); - assert.equal(terminal1Data, '123'); - assert.equal(terminal2Counter, 1); - assert.equal(terminal2Data, '4567'); + assert.equal(counter[1], 1); + assert.equal(data[1], '123'); + assert.equal(counter[2], 1); + assert.equal(data[2], '4567'); }); test('stop', async () => { let terminalOnData = new Emitter(); - let counter = 0; - let data: string | undefined; - bufferer.startBuffering(1, terminalOnData.event, (id, e) => { - counter++; - data = e; - }, 0); + bufferer.startBuffering(1, terminalOnData.event, 0); terminalOnData.fire('1'); terminalOnData.fire('2'); terminalOnData.fire('3'); bufferer.stopBuffering(1); - await wait(0); - assert.equal(counter, 0); - assert.equal(data, undefined); + assert.equal(counter[1], 1); + assert.equal(data[1], '123'); }); test('start 2 stop 1', async () => { - let terminal1OnData = new Emitter(); - let terminal1Counter = 0; - let terminal1Data: string | undefined; - - bufferer.startBuffering(1, terminal1OnData.event, (id, e) => { - terminal1Counter++; - terminal1Data = e; - }, 0); - - let terminal2OnData = new Emitter(); - let terminal2Counter = 0; - let terminal2Data: string | undefined; - - bufferer.startBuffering(2, terminal2OnData.event, (id, e) => { - terminal2Counter++; - terminal2Data = e; - }, 0); + const terminal1OnData = new Emitter(); + const terminal2OnData = new Emitter(); + bufferer.startBuffering(1, terminal1OnData.event, 0); + bufferer.startBuffering(2, terminal2OnData.event, 0); terminal1OnData.fire('1'); terminal2OnData.fire('4'); @@ -133,39 +110,26 @@ suite('Workbench - TerminalDataBufferer', () => { terminal2OnData.fire('6'); terminal2OnData.fire('7'); - assert.equal(terminal1Counter, 0); - assert.equal(terminal1Data, undefined); - assert.equal(terminal2Counter, 0); - assert.equal(terminal2Data, undefined); + assert.equal(counter[1], undefined); + assert.equal(data[1], undefined); + assert.equal(counter[2], undefined); + assert.equal(data[2], undefined); bufferer.stopBuffering(1); await wait(0); - assert.equal(terminal1Counter, 0); - assert.equal(terminal1Data, undefined); - assert.equal(terminal2Counter, 1); - assert.equal(terminal2Data, '4567'); + assert.equal(counter[1], 1); + assert.equal(data[1], '123'); + assert.equal(counter[2], 1); + assert.equal(data[2], '4567'); }); - test('dispose', async () => { - let terminal1OnData = new Emitter(); - let terminal1Counter = 0; - let terminal1Data: string | undefined; - - bufferer.startBuffering(1, terminal1OnData.event, (id, e) => { - terminal1Counter++; - terminal1Data = e; - }, 0); - - let terminal2OnData = new Emitter(); - let terminal2Counter = 0; - let terminal2Data: string | undefined; - - bufferer.startBuffering(2, terminal2OnData.event, (id, e) => { - terminal2Counter++; - terminal2Data = e; - }, 0); + test('dispose should flush remaining data events', async () => { + const terminal1OnData = new Emitter(); + const terminal2OnData = new Emitter(); + bufferer.startBuffering(1, terminal1OnData.event, 0); + bufferer.startBuffering(2, terminal2OnData.event, 0); terminal1OnData.fire('1'); terminal2OnData.fire('4'); @@ -175,17 +139,17 @@ suite('Workbench - TerminalDataBufferer', () => { terminal2OnData.fire('6'); terminal2OnData.fire('7'); - assert.equal(terminal1Counter, 0); - assert.equal(terminal1Data, undefined); - assert.equal(terminal2Counter, 0); - assert.equal(terminal2Data, undefined); + assert.equal(counter[1], undefined); + assert.equal(data[1], undefined); + assert.equal(counter[2], undefined); + assert.equal(data[2], undefined); bufferer.dispose(); await wait(0); - assert.equal(terminal1Counter, 0); - assert.equal(terminal1Data, undefined); - assert.equal(terminal2Counter, 0); - assert.equal(terminal2Data, undefined); + assert.equal(counter[1], 1); + assert.equal(data[1], '123'); + assert.equal(counter[2], 1); + assert.equal(data[2], '4567'); }); }); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSync.ts index 2b7c856d77..f87291c3d1 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSync.ts @@ -3,9 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserDataSyncService, IUserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IUserDataSyncLogService, IUserDataAuthTokenService, IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IAuthTokenService } from 'vs/platform/auth/common/auth'; import { Event } from 'vs/base/common/event'; import { UserDataAutoSync as BaseUserDataAutoSync } from 'vs/platform/userDataSync/common/userDataAutoSync'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -18,18 +17,19 @@ export class UserDataAutoSync extends BaseUserDataAutoSync { @IUserDataSyncService userDataSyncService: IUserDataSyncService, @IConfigurationService configurationService: IConfigurationService, @IUserDataSyncLogService logService: IUserDataSyncLogService, - @IAuthTokenService authTokenService: IAuthTokenService, + @IUserDataAuthTokenService authTokenService: IUserDataAuthTokenService, @IInstantiationService instantiationService: IInstantiationService, @IHostService hostService: IHostService, + @IUserDataSyncUtilService userDataSyncUtilService: IUserDataSyncUtilService, ) { - super(configurationService, userDataSyncService, logService, authTokenService); + super(configurationService, userDataSyncService, logService, authTokenService, userDataSyncUtilService); // Sync immediately if there is a local change. this._register(Event.debounce(Event.any( userDataSyncService.onDidChangeLocal, instantiationService.createInstance(UserDataSyncTrigger).onDidTriggerSync, hostService.onDidChangeFocus - ), () => undefined, 500)(() => this.sync(false))); + ), () => undefined, 500)(() => this.triggerAutoSync())); } } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 12986acb43..52ffc5f521 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IUserDataSyncService, SyncStatus, SyncSource, CONTEXT_SYNC_STATE, IUserDataSyncStore, registerConfiguration, getUserDataSyncStore } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, SyncStatus, SyncSource, CONTEXT_SYNC_STATE, IUserDataSyncStore, registerConfiguration, getUserDataSyncStore, ISyncConfiguration, IUserDataAuthTokenService, IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; import { localize } from 'vs/nls'; import { Disposable, MutableDisposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; @@ -24,34 +24,46 @@ import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { isEqual } from 'vs/base/common/resources'; import { IEditorInput } from 'vs/workbench/common/editor'; -import { IAuthTokenService, AuthTokenStatus } from 'vs/platform/auth/common/auth'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { FalseContext } from 'vs/platform/contextkey/common/contextkeys'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { isWeb } from 'vs/base/common/platform'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { UserDataAutoSync } from 'vs/workbench/contrib/userDataSync/browser/userDataAutoSync'; import { UserDataSyncTrigger } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger'; import { timeout } from 'vs/base/common/async'; +import { IOutputService } from 'vs/workbench/contrib/output/common/output'; +import * as Constants from 'vs/workbench/contrib/logs/common/logConstants'; +import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; +import { Session } from 'vs/editor/common/modes'; +import { isPromiseCanceledError, canceled } from 'vs/base/common/errors'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; -const CONTEXT_AUTH_TOKEN_STATE = new RawContextKey('authTokenStatus', AuthTokenStatus.Initializing); +const enum AuthStatus { + Initializing = 'Initializing', + SignedIn = 'SignedIn', + SignedOut = 'SignedOut' +} +const CONTEXT_AUTH_TOKEN_STATE = new RawContextKey('authTokenStatus', AuthStatus.Initializing); const SYNC_PUSH_LIGHT_ICON_URI = URI.parse(registerAndGetAmdImageURL(`vs/workbench/contrib/userDataSync/browser/media/check-light.svg`)); const SYNC_PUSH_DARK_ICON_URI = URI.parse(registerAndGetAmdImageURL(`vs/workbench/contrib/userDataSync/browser/media/check-dark.svg`)); +type ConfigureSyncQuickPickItem = { id: string, label: string, description?: string }; + export class UserDataSyncWorkbenchContribution extends Disposable implements IWorkbenchContribution { private static readonly ENABLEMENT_SETTING = 'sync.enable'; private readonly userDataSyncStore: IUserDataSyncStore | undefined; private readonly syncStatusContext: IContextKey; - private readonly authTokenContext: IContextKey; + private readonly authenticationState: IContextKey; private readonly badgeDisposable = this._register(new MutableDisposable()); private readonly conflictsWarningDisposable = this._register(new MutableDisposable()); private readonly signInNotificationDisposable = this._register(new MutableDisposable()); + private _activeAccount: Session | undefined; constructor( @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, - @IAuthTokenService private readonly authTokenService: IAuthTokenService, + @IAuthenticationService private readonly authenticationService: IAuthenticationService, @IContextKeyService contextKeyService: IContextKeyService, @IActivityService private readonly activityService: IActivityService, @INotificationService private readonly notificationService: INotificationService, @@ -63,43 +75,104 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo @IDialogService private readonly dialogService: IDialogService, @IQuickInputService private readonly quickInputService: IQuickInputService, @IInstantiationService instantiationService: IInstantiationService, + @IOutputService private readonly outputService: IOutputService, + @IUserDataAuthTokenService private readonly userDataAuthTokenService: IUserDataAuthTokenService, + @IUserDataAutoSyncService userDataAutoSyncService: IUserDataAutoSyncService, ) { super(); this.userDataSyncStore = getUserDataSyncStore(configurationService); this.syncStatusContext = CONTEXT_SYNC_STATE.bindTo(contextKeyService); - this.authTokenContext = CONTEXT_AUTH_TOKEN_STATE.bindTo(contextKeyService); - + this.authenticationState = CONTEXT_AUTH_TOKEN_STATE.bindTo(contextKeyService); if (this.userDataSyncStore) { registerConfiguration(); - this.onDidChangeAuthTokenStatus(this.authTokenService.status); this.onDidChangeSyncStatus(this.userDataSyncService.status); - this._register(Event.debounce(authTokenService.onDidChangeStatus, () => undefined, 500)(() => this.onDidChangeAuthTokenStatus(this.authTokenService.status))); this._register(Event.debounce(userDataSyncService.onDidChangeStatus, () => undefined, 500)(() => this.onDidChangeSyncStatus(this.userDataSyncService.status))); this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(UserDataSyncWorkbenchContribution.ENABLEMENT_SETTING))(() => this.onDidChangeEnablement())); + this._register(this.authenticationService.onDidRegisterAuthenticationProvider(e => this.onDidRegisterAuthenticationProvider(e))); + this._register(this.authenticationService.onDidUnregisterAuthenticationProvider(e => this.onDidUnregisterAuthenticationProvider(e))); + this._register(this.authenticationService.onDidChangeSessions(e => this.onDidChangeSessions(e))); this.registerActions(); + this.initializeActiveAccount().then(_ => { + if (isWeb) { + this._register(instantiationService.createInstance(UserDataAutoSync)); + } else { + this._register(instantiationService.createInstance(UserDataSyncTrigger).onDidTriggerSync(() => userDataAutoSyncService.triggerAutoSync())); + } + }); + } + } - if (isWeb) { - this._register(instantiationService.createInstance(UserDataAutoSync)); + private async initializeActiveAccount(): Promise { + const accounts = await this.authenticationService.getSessions(this.userDataSyncStore!.authenticationProviderId); + // Auth provider has not yet been registered + if (!accounts) { + return; + } + + if (accounts.length === 0) { + this.activeAccount = undefined; + return; + } + + if (accounts.length === 1) { + this.activeAccount = accounts[0]; + return; + } + + const selectedAccount = await this.quickInputService.pick(accounts.map(account => { + return { + id: account.id, + label: account.displayName + }; + }), { canPickMany: false }); + + if (selectedAccount) { + this.activeAccount = accounts.filter(account => selectedAccount.id === account.id)[0]; + } + } + + get activeAccount(): Session | undefined { + return this._activeAccount; + } + + set activeAccount(account: Session | undefined) { + this._activeAccount = account; + + if (account) { + this.userDataAuthTokenService.setToken(account.accessToken); + this.authenticationState.set(AuthStatus.SignedIn); + } else { + this.userDataAuthTokenService.setToken(undefined); + this.authenticationState.set(AuthStatus.SignedOut); + } + + this.updateBadge(); + } + + private async onDidChangeSessions(providerId: string): Promise { + if (providerId === this.userDataSyncStore!.authenticationProviderId) { + if (this.activeAccount) { + // Try to update existing account, case where access token has been refreshed + const accounts = (await this.authenticationService.getSessions(this.userDataSyncStore!.authenticationProviderId) || []); + const matchingAccount = accounts.filter(a => a.id === this.activeAccount?.id)[0]; + this.activeAccount = matchingAccount; } else { - this._register(instantiationService.createInstance(UserDataSyncTrigger).onDidTriggerSync(() => this.triggerSync())); + this.initializeActiveAccount(); } } } - private triggerSync(): void { - if (this.configurationService.getValue('sync.enable') - && this.userDataSyncService.status !== SyncStatus.Uninitialized - && this.authTokenService.status === AuthTokenStatus.SignedIn) { - this.userDataSyncService.sync(); + private async onDidRegisterAuthenticationProvider(providerId: string) { + if (providerId === this.userDataSyncStore!.authenticationProviderId) { + await this.initializeActiveAccount(); } } - private onDidChangeAuthTokenStatus(status: AuthTokenStatus) { - this.authTokenContext.set(status); - if (status === AuthTokenStatus.SignedIn) { - this.signInNotificationDisposable.clear(); + private onDidUnregisterAuthenticationProvider(providerId: string) { + if (providerId === this.userDataSyncStore!.authenticationProviderId) { + this.activeAccount = undefined; + this.authenticationState.reset(); } - this.updateBadge(); } private onDidChangeSyncStatus(status: SyncStatus) { @@ -120,7 +193,11 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo label: localize('resolve', "Resolve Conflicts"), run: () => this.handleConflicts() } - ]); + ], + { + sticky: true + } + ); this.conflictsWarningDisposable.value = toDisposable(() => handle.close()); handle.onDidClose(() => this.conflictsWarningDisposable.clear()); } @@ -137,8 +214,8 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo this.updateBadge(); const enabled = this.configurationService.getValue(UserDataSyncWorkbenchContribution.ENABLEMENT_SETTING); if (enabled) { - if (this.authTokenService.status === AuthTokenStatus.SignedOut) { - const handle = this.notificationService.prompt(Severity.Info, localize('ask to sign in', "Please sign in with your {0} account to sync configuration across all your machines", this.userDataSyncStore!.account), + if (this.authenticationState.get() === AuthStatus.SignedOut) { + const handle = this.notificationService.prompt(Severity.Info, localize('sign in message', "Please sign in with your {0} account to continue sync", this.userDataSyncStore!.account), [ { label: localize('Sign in', "Sign in"), @@ -153,19 +230,15 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } } - private updateBadge(): void { + private async updateBadge(): Promise { this.badgeDisposable.clear(); let badge: IBadge | undefined = undefined; let clazz: string | undefined; let priority: number | undefined = undefined; - if (this.userDataSyncService.status !== SyncStatus.Uninitialized && this.configurationService.getValue(UserDataSyncWorkbenchContribution.ENABLEMENT_SETTING) && this.authTokenService.status === AuthTokenStatus.SignedOut) { + if (this.userDataSyncService.status !== SyncStatus.Uninitialized && this.configurationService.getValue(UserDataSyncWorkbenchContribution.ENABLEMENT_SETTING) && this.authenticationState.get() === AuthStatus.SignedOut) { badge = new NumberBadge(1, () => localize('sign in to sync', "Sign in to Sync")); - } else if (this.authTokenService.status === AuthTokenStatus.SigningIn) { - badge = new ProgressBadge(() => localize('signing in', "Signing in...")); - clazz = 'progress-badge'; - priority = 1; } else if (this.userDataSyncService.status === SyncStatus.HasConflicts) { badge = new NumberBadge(1, () => localize('resolve conflicts', "Resolve Conflicts")); } else if (this.userDataSyncService.status === SyncStatus.Syncing) { @@ -180,53 +253,90 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } private async turnOn(): Promise { - if (this.authTokenService.status === AuthTokenStatus.SignedOut) { - const result = await this.dialogService.confirm({ - type: 'info', - message: localize('sign in to account', "Sign in to {0}", this.userDataSyncStore!.name), - detail: localize('ask to sign in', "Please sign in with your {0} account to sync configuration across all your machines", this.userDataSyncStore!.account), - primaryButton: localize('Sign in', "Sign in") - }); - if (!result.confirmed) { - return; - } - await this.signIn(); - } - await this.configureSyncOptions(); - await this.configurationService.updateValue(UserDataSyncWorkbenchContribution.ENABLEMENT_SETTING, true); - this.notificationService.info(localize('Sync Started', "Sync Started.")); - } - - private async configureSyncOptions(): Promise { return new Promise((c, e) => { const disposables: DisposableStore = new DisposableStore(); - const quickPick = this.quickInputService.createQuickPick(); + const quickPick = this.quickInputService.createQuickPick(); disposables.add(quickPick); - quickPick.title = localize('configure sync title', "Sync: Configure"); - quickPick.placeholder = localize('select configurations to sync', "Choose what to sync"); + quickPick.title = localize('turn on sync', "Turn on Sync"); + quickPick.ok = false; + quickPick.customButton = true; + if (this.authenticationState.get() === AuthStatus.SignedIn) { + quickPick.description = localize('turn on sync detail', "Turn on to synchronize your following data across all your devices."); + quickPick.customLabel = localize('turn on', "Turn on"); + } else { + 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.", this.userDataSyncStore!.account); + quickPick.customLabel = localize('sign in and turn on sync', "Sign in & Turn on"); + } + quickPick.placeholder = localize('configure sync placeholder', "Choose what to sync"); quickPick.canSelectMany = true; quickPick.ignoreFocusOut = true; - const items = [{ - id: 'sync.enableSettings', - label: localize('user settings', "User Settings") - }, { - id: 'sync.enableKeybindings', - label: localize('user keybindings', "User Keybindings") - }, { - id: 'sync.enableExtensions', - label: localize('extensions', "Extensions") - }]; + const items = this.getConfigureSyncQuickPickItems(); quickPick.items = items; quickPick.selectedItems = items.filter(item => this.configurationService.getValue(item.id)); - disposables.add(quickPick.onDidAccept(() => { - for (const item of items) { - const wasEnabled = this.configurationService.getValue(item.id); - const isEnabled = !!quickPick.selectedItems.filter(selected => selected.id === item.id)[0]; - if (wasEnabled !== isEnabled) { - this.configurationService.updateValue(item.id!, isEnabled); - } + disposables.add(Event.any(quickPick.onDidAccept, quickPick.onDidCustom)(async () => { + if (quickPick.selectedItems.length) { + await this.updateConfiguration(items, quickPick.selectedItems); + this.doTurnOn().then(c, e); + quickPick.hide(); + } + })); + disposables.add(quickPick.onDidHide(() => disposables.dispose())); + quickPick.show(); + }); + } + + private async doTurnOn(): Promise { + if (this.authenticationState.get() === AuthStatus.SignedOut) { + await this.signIn(); + } + await this.handleFirstTimeSync(); + await this.enableSync(); + } + + private getConfigureSyncQuickPickItems(): ConfigureSyncQuickPickItem[] { + return [{ + id: 'sync.enableSettings', + label: localize('settings', "Settings") + }, { + id: 'sync.enableKeybindings', + label: localize('keybindings', "Keybindings") + }, { + id: 'sync.enableExtensions', + label: localize('extensions', "Extensions") + }, { + id: 'sync.enableUIState', + label: localize('ui state label', "UI State"), + description: localize('ui state description', "Display Language (Only)") + }]; + } + + private async updateConfiguration(items: ConfigureSyncQuickPickItem[], selectedItems: ReadonlyArray): Promise { + for (const item of items) { + const wasEnabled = this.configurationService.getValue(item.id); + const isEnabled = !!selectedItems.filter(selected => selected.id === item.id)[0]; + if (wasEnabled !== isEnabled) { + await this.configurationService.updateValue(item.id!, isEnabled); + } + } + } + + private async configureSyncOptions(): Promise { + 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.placeholder = localize('configure sync placeholder', "Choose what to sync"); + quickPick.canSelectMany = true; + quickPick.ignoreFocusOut = true; + const items = this.getConfigureSyncQuickPickItems(); + quickPick.items = items; + quickPick.selectedItems = items.filter(item => this.configurationService.getValue(item.id)); + disposables.add(quickPick.onDidAccept(async () => { + if (quickPick.selectedItems.length) { + await this.updateConfiguration(items, quickPick.selectedItems); + quickPick.hide(); } - quickPick.hide(); })); disposables.add(quickPick.onDidHide(() => { disposables.dispose(); @@ -236,13 +346,60 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo }); } + private async handleFirstTimeSync(): Promise { + const hasRemote = await this.userDataSyncService.hasRemoteData(); + if (!hasRemote) { + return; + } + const isFirstSyncAndHasUserData = await this.userDataSyncService.isFirstTimeSyncAndHasUserData(); + if (!isFirstSyncAndHasUserData) { + return; + } + const result = await this.dialogService.show( + Severity.Info, + localize('firs time sync', "First time Sync"), + [ + localize('merge', "Merge"), + localize('cancel', "Cancel"), + localize('replace', "Replace (Overwrite Local)"), + ], + { + 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 cloud?"), + } + ); + switch (result.choice) { + case 0: await this.userDataSyncService.sync(); break; + case 1: throw canceled(); + case 2: await this.userDataSyncService.pull(); break; + } + } + + private enableSync(): Promise { + return this.configurationService.updateValue(UserDataSyncWorkbenchContribution.ENABLEMENT_SETTING, true); + } + private async turnOff(): Promise { - await this.configurationService.updateValue(UserDataSyncWorkbenchContribution.ENABLEMENT_SETTING, false); + const result = await this.dialogService.confirm({ + type: 'info', + message: localize('turn off sync confirmation', "Turn off Sync"), + detail: localize('turn off sync detail', "Your settings, keybindings, extensions and more will no longer be synced."), + primaryButton: localize('turn off', "Turn off"), + checkbox: { + label: localize('turn off sync everywhere', "Turn off sync in all your devices and clear the data in cloud.") + } + }); + if (result.confirmed) { + await this.configurationService.updateValue(UserDataSyncWorkbenchContribution.ENABLEMENT_SETTING, false); + if (result.checkboxChecked) { + await this.userDataSyncService.reset(); + } + } } private async signIn(): Promise { try { - await this.authTokenService.login(); + this.activeAccount = await this.authenticationService.login(this.userDataSyncStore!.authenticationProviderId); } catch (e) { this.notificationService.error(e); throw e; @@ -250,7 +407,10 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } private async signOut(): Promise { - await this.authTokenService.logout(); + if (this.activeAccount) { + await this.authenticationService.logout(this.userDataSyncStore!.authenticationProviderId, this.activeAccount.id); + this.activeAccount = undefined; + } } private async continueSync(): Promise { @@ -313,11 +473,23 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo return null; } + private showSyncLog(): Promise { + return this.outputService.showChannel(Constants.userDataSyncLogChannelId); + } + private registerActions(): void { const turnOnSyncCommandId = 'workbench.userData.actions.syncStart'; - const turnOnSyncWhenContext = ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), ContextKeyExpr.not(`config.${UserDataSyncWorkbenchContribution.ENABLEMENT_SETTING}`), CONTEXT_AUTH_TOKEN_STATE.notEqualsTo(AuthTokenStatus.SigningIn)); - CommandsRegistry.registerCommand(turnOnSyncCommandId, () => this.turnOn()); + const turnOnSyncWhenContext = ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), ContextKeyExpr.not(`config.${UserDataSyncWorkbenchContribution.ENABLEMENT_SETTING}`), CONTEXT_AUTH_TOKEN_STATE.notEqualsTo(AuthStatus.Initializing)); + CommandsRegistry.registerCommand(turnOnSyncCommandId, async () => { + try { + await this.turnOn(); + } catch (e) { + if (!isPromiseCanceledError(e)) { + this.notificationService.error(localize('turn on failed', "Error while starting Sync: {0}", toErrorMessage(e))); + } + } + }); MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { group: '5_sync', command: { @@ -329,13 +501,13 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: turnOnSyncCommandId, - title: localize('turn on sync', "Sync: Turn on sync...") + title: localize('turn on sync...', "Sync: Turn on sync...") }, when: turnOnSyncWhenContext, }); const signInCommandId = 'workbench.userData.actions.signin'; - const signInWhenContext = ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), ContextKeyExpr.has(`config.${UserDataSyncWorkbenchContribution.ENABLEMENT_SETTING}`), CONTEXT_AUTH_TOKEN_STATE.isEqualTo(AuthTokenStatus.SignedOut)); + const signInWhenContext = ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), ContextKeyExpr.has(`config.${UserDataSyncWorkbenchContribution.ENABLEMENT_SETTING}`), CONTEXT_AUTH_TOKEN_STATE.isEqualTo(AuthStatus.SignedOut)); CommandsRegistry.registerCommand(signInCommandId, () => this.signIn()); MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { group: '5_sync', @@ -353,18 +525,6 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo when: signInWhenContext, }); - const signingInCommandId = 'workbench.userData.actions.signingin'; - CommandsRegistry.registerCommand(signingInCommandId, () => null); - MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { - group: '5_sync', - command: { - id: signingInCommandId, - title: localize('signinig in', "Signing in..."), - precondition: FalseContext - }, - when: CONTEXT_AUTH_TOKEN_STATE.isEqualTo(AuthTokenStatus.SigningIn) - }); - const stopSyncCommandId = 'workbench.userData.actions.stopSync'; CommandsRegistry.registerCommand(stopSyncCommandId, () => this.turnOff()); MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { @@ -373,7 +533,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo id: stopSyncCommandId, title: localize('global activity stop sync', "Turn off sync") }, - when: ContextKeyExpr.and(ContextKeyExpr.has(`config.${UserDataSyncWorkbenchContribution.ENABLEMENT_SETTING}`), CONTEXT_AUTH_TOKEN_STATE.isEqualTo(AuthTokenStatus.SignedIn), CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.HasConflicts)) + when: ContextKeyExpr.and(ContextKeyExpr.has(`config.${UserDataSyncWorkbenchContribution.ENABLEMENT_SETTING}`), CONTEXT_AUTH_TOKEN_STATE.isEqualTo(AuthStatus.SignedIn), CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.HasConflicts)) }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { @@ -444,9 +604,39 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo id: 'workbench.userData.actions.signout', title: localize('sign out', "Sync: Sign out") }, - when: ContextKeyExpr.and(CONTEXT_AUTH_TOKEN_STATE.isEqualTo(AuthTokenStatus.SignedIn)), + when: ContextKeyExpr.and(CONTEXT_AUTH_TOKEN_STATE.isEqualTo(AuthStatus.SignedIn)), }; CommandsRegistry.registerCommand(signOutMenuItem.command.id, () => this.signOut()); MenuRegistry.appendMenuItem(MenuId.CommandPalette, signOutMenuItem); + + const configureSyncCommandId = 'workbench.userData.actions.configureSync'; + CommandsRegistry.registerCommand(configureSyncCommandId, () => this.configureSyncOptions()); + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: configureSyncCommandId, + title: localize('configure sync', "Sync: Configure") + }, + when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), ContextKeyExpr.has(`config.${UserDataSyncWorkbenchContribution.ENABLEMENT_SETTING}`)), + }); + + const showSyncLogCommandId = 'workbench.userData.actions.showSyncLog'; + CommandsRegistry.registerCommand(showSyncLogCommandId, () => this.showSyncLog()); + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: showSyncLogCommandId, + title: localize('show sync log', "Sync: Show Sync Log") + }, + when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized)), + }); + + const resetLocalCommandId = 'workbench.userData.actions.resetLocal'; + CommandsRegistry.registerCommand(resetLocalCommandId, () => this.userDataSyncService.resetLocal()); + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: resetLocalCommandId, + title: localize('reset local', "Developer: Reset Local (Sync)") + }, + when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized)), + }); } } diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index 32e0db6012..2c8d59304d 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -61,7 +61,7 @@ export class IFrameWebview extends BaseWebview implements Web protected createElement(options: WebviewOptions) { const element = document.createElement('iframe'); element.className = `webview ${options.customClasses || ''}`; - element.sandbox.add('allow-scripts', 'allow-same-origin'); + element.sandbox.add('allow-scripts', 'allow-same-origin', 'allow-forms'); element.setAttribute('src', `${this.externalEndpoint}/index.html?id=${this.id}`); element.style.border = 'none'; element.style.width = '100%'; diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts index 6d8ae3892b..191b00697a 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { FindInPageOptions, OnBeforeRequestDetails, OnHeadersReceivedDetails, Response, WebContents, WebviewTag } from 'electron'; +import { FindInPageOptions, OnBeforeRequestListenerDetails, OnHeadersReceivedListenerDetails, Response, WebContents, WebviewTag } from 'electron'; import { addDisposableListener } from 'vs/base/browser/dom'; import { Emitter, Event } from 'vs/base/common/event'; import { once } from 'vs/base/common/functional'; @@ -65,8 +65,8 @@ class WebviewTagHandle extends Disposable { } } -type OnBeforeRequestDelegate = (details: OnBeforeRequestDetails) => Promise; -type OnHeadersReceivedDelegate = (details: OnHeadersReceivedDetails) => { cancel: boolean; } | undefined; +type OnBeforeRequestDelegate = (details: OnBeforeRequestListenerDetails) => Promise; +type OnHeadersReceivedDelegate = (details: OnHeadersReceivedListenerDetails) => { cancel: boolean; } | undefined; class WebviewSession extends Disposable { diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewProtocols.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewProtocols.ts index fa8d568edc..7bb1e2e769 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewProtocols.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewProtocols.ts @@ -32,10 +32,6 @@ export function registerFileProtocol( } return callback({ error: -2 /* FAILED: https://cs.chromium.org/chromium/src/net/base/net_error_list.h */ }); - }, (error) => { - if (error) { - console.error(`Failed to register '${protocol}' protocol`); - } }); } diff --git a/src/vs/workbench/contrib/welcome/page/browser/vs_code_welcome_page.ts b/src/vs/workbench/contrib/welcome/page/browser/vs_code_welcome_page.ts index 4e70f372bc..d1967481f2 100644 --- a/src/vs/workbench/contrib/welcome/page/browser/vs_code_welcome_page.ts +++ b/src/vs/workbench/contrib/welcome/page/browser/vs_code_welcome_page.ts @@ -66,7 +66,7 @@ export default () => `
-
+
diff --git a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput.ts b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput.ts index 2ca566f8d1..f7b6446009 100644 --- a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput.ts +++ b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput.ts @@ -11,6 +11,7 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import * as marked from 'vs/base/common/marked/marked'; import { Schemas } from 'vs/base/common/network'; import { isEqual } from 'vs/base/common/resources'; +import { EndOfLinePreference } from 'vs/editor/common/model'; export class WalkThroughModel extends EditorModel { @@ -111,7 +112,7 @@ export class WalkThroughInput extends EditorInput { return ''; }; - const markdown = ref.object.textEditorModel.getLinesContent().join('\n'); + const markdown = ref.object.textEditorModel.getValue(EndOfLinePreference.LF); marked(markdown, { renderer }); return Promise.all(snippets) diff --git a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts index fc58876023..33801fb1ff 100644 --- a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts +++ b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts @@ -39,6 +39,7 @@ import { Dimension, size } from 'vs/base/browser/dom'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { domEvent } from 'vs/base/browser/event'; +import { EndOfLinePreference } from 'vs/editor/common/model'; export const WALK_THROUGH_FOCUS = new RawContextKey('interactivePlaygroundFocus', false); @@ -278,7 +279,7 @@ export class WalkThroughPart extends BaseEditor { return; } - const content = model.main.textEditorModel.getLinesContent().join('\n'); + const content = model.main.textEditorModel.getValue(EndOfLinePreference.LF); if (!strings.endsWith(input.getResource().path, '.md')) { this.content.innerHTML = content; this.updateSizeClasses(); @@ -421,7 +422,8 @@ export class WalkThroughPart extends BaseEditor { horizontal: 'auto', useShadows: true, verticalHasArrows: false, - horizontalHasArrows: false + horizontalHasArrows: false, + alwaysConsumeMouseWheel: false }, overviewRulerLanes: 3, fixedOverflowWidgets: true, diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index 8ff5d53b0f..a469292ebe 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -21,7 +21,7 @@ import * as browser from 'vs/base/browser/browser'; import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IResourceInput } from 'vs/platform/editor/common/editor'; import { KeyboardMapperFactory } from 'vs/workbench/services/keybinding/electron-browser/nativeKeymapService'; -import { ipcRenderer as ipc, webFrame, crashReporter, Event as IpcEvent } from 'electron'; +import { ipcRenderer as ipc, webFrame, crashReporter, CrashReporterStartOptions, Event as IpcEvent } from 'electron'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; import { IMenuService, MenuId, IMenu, MenuItemAction, ICommandAction, SubmenuItemAction, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -41,7 +41,6 @@ import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessi import { WorkbenchState, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { coalesce } from 'vs/base/common/arrays'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { isEqual } from 'vs/base/common/resources'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { MenubarControl } from '../browser/parts/titlebar/menubarControl'; @@ -98,7 +97,6 @@ export class ElectronWindow extends Disposable { @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @ITextFileService private readonly textFileService: ITextFileService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IOpenerService private readonly openerService: IOpenerService, @IElectronService private readonly electronService: IElectronService, @@ -270,7 +268,7 @@ export class ElectronWindow extends Disposable { if (isMacintosh) { this._register(this.workingCopyService.onDidChangeDirty(workingCopy => { const gotDirty = workingCopy.isDirty(); - if (gotDirty && !!(workingCopy.capabilities & WorkingCopyCapabilities.AutoSave) && this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) { + if (gotDirty && !(workingCopy.capabilities & WorkingCopyCapabilities.Untitled) && this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) { return; // do not indicate dirty of working copies that are auto saved after short delay } @@ -539,13 +537,13 @@ export class ElectronWindow extends Disposable { } // base options with product info - const options = { + const options: CrashReporterStartOptions = { companyName, productName, submitURL: isWindows ? hockeyAppConfig[process.arch === 'ia32' ? 'win32-ia32' : 'win32-x64'] : isLinux ? hockeyAppConfig[`linux-x64`] : hockeyAppConfig.darwin, extra: { vscode_version: product.version, - vscode_commit: product.commit + vscode_commit: product.commit || '' } }; @@ -626,7 +624,7 @@ export class ElectronWindow extends Disposable { // to close the editor while the save still continues in the background. As such // we have to also check if the files to wait for are dirty and if so wait // for them to get saved before deleting the wait marker file. - const dirtyFilesToWait = this.textFileService.getDirty(resourcesToWaitFor); + const dirtyFilesToWait = resourcesToWaitFor.filter(resourceToWaitFor => this.workingCopyService.isDirty(resourceToWaitFor)); if (dirtyFilesToWait.length > 0) { await Promise.all(dirtyFilesToWait.map(async dirtyFileToWait => await this.joinResourceSaved(dirtyFileToWait))); } @@ -641,13 +639,13 @@ export class ElectronWindow extends Disposable { private joinResourceSaved(resource: URI): Promise { return new Promise(resolve => { - if (!this.textFileService.isDirty(resource)) { + if (!this.workingCopyService.isDirty(resource)) { return resolve(); // return early if resource is not dirty } // Otherwise resolve promise when resource is saved - const listener = this.textFileService.models.onModelSaved(e => { - if (isEqual(resource, e.resource)) { + const listener = this.workingCopyService.onDidChangeDirty(e => { + if (!e.isDirty() && isEqual(resource, e.resource)) { listener.dispose(); resolve(); diff --git a/src/vs/workbench/services/authToken/browser/authTokenService.ts b/src/vs/workbench/services/authToken/browser/authTokenService.ts deleted file mode 100644 index 3622da7a0d..0000000000 --- a/src/vs/workbench/services/authToken/browser/authTokenService.ts +++ /dev/null @@ -1,74 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { localize } from 'vs/nls'; -import { Event, Emitter } from 'vs/base/common/event'; -import { IAuthTokenService, AuthTokenStatus } from 'vs/platform/auth/common/auth'; -import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { URI } from 'vs/base/common/uri'; - -const SERVICE_NAME = 'VS Code'; -const ACCOUNT = 'MyAccount'; - -export class AuthTokenService extends Disposable implements IAuthTokenService { - _serviceBrand: undefined; - - private _status: AuthTokenStatus = AuthTokenStatus.Initializing; - get status(): AuthTokenStatus { return this._status; } - private _onDidChangeStatus: Emitter = this._register(new Emitter()); - readonly onDidChangeStatus: Event = this._onDidChangeStatus.event; - - readonly _onDidGetCallback: Emitter = this._register(new Emitter()); - - constructor( - @ICredentialsService private readonly credentialsService: ICredentialsService, - @IQuickInputService private readonly quickInputService: IQuickInputService - ) { - super(); - this.getToken().then(token => { - if (token) { - this.setStatus(AuthTokenStatus.SignedIn); - } else { - this.setStatus(AuthTokenStatus.SignedOut); - } - }); - } - - async getToken(): Promise { - const token = await this.credentialsService.getPassword(SERVICE_NAME, ACCOUNT); - if (token) { - return token; - } - - return undefined; // {{SQL CARBON EDIT}} strict-null-check - } - - async login(): Promise { - const token = await this.quickInputService.input({ placeHolder: localize('enter token', "Please provide the auth bearer token"), ignoreFocusLost: true, }); - if (token) { - await this.credentialsService.setPassword(SERVICE_NAME, ACCOUNT, token); - this.setStatus(AuthTokenStatus.SignedIn); - } - } - - async refreshToken(): Promise { - await this.logout(); - } - - async logout(): Promise { - await this.credentialsService.deletePassword(SERVICE_NAME, ACCOUNT); - this.setStatus(AuthTokenStatus.SignedOut); - } - - private setStatus(status: AuthTokenStatus): void { - if (this._status !== status) { - this._status = status; - this._onDidChangeStatus.fire(status); - } - } - -} diff --git a/src/vs/workbench/services/authToken/electron-browser/authTokenService.ts b/src/vs/workbench/services/authToken/electron-browser/authTokenService.ts deleted file mode 100644 index 4719b29741..0000000000 --- a/src/vs/workbench/services/authToken/electron-browser/authTokenService.ts +++ /dev/null @@ -1,61 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { Emitter, Event } from 'vs/base/common/event'; -import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IAuthTokenService, AuthTokenStatus } from 'vs/platform/auth/common/auth'; -import { URI } from 'vs/base/common/uri'; - -export class AuthTokenService extends Disposable implements IAuthTokenService { - - _serviceBrand: undefined; - - private readonly channel: IChannel; - - private _status: AuthTokenStatus = AuthTokenStatus.Initializing; - get status(): AuthTokenStatus { return this._status; } - private _onDidChangeStatus: Emitter = this._register(new Emitter()); - readonly onDidChangeStatus: Event = this._onDidChangeStatus.event; - - readonly _onDidGetCallback: Emitter = this._register(new Emitter()); - - constructor( - @ISharedProcessService sharedProcessService: ISharedProcessService, - ) { - super(); - this.channel = sharedProcessService.getChannel('authToken'); - this._register(this.channel.listen('onDidChangeStatus')(status => this.updateStatus(status))); - this.channel.call('_getInitialStatus').then(status => this.updateStatus(status)); - } - - getToken(): Promise { - return this.channel.call('getToken'); - } - - login(): Promise { - return this.channel.call('login'); - } - - refreshToken(): Promise { - return this.channel.call('getToken'); - } - - logout(): Promise { - return this.channel.call('logout'); - } - - private async updateStatus(status: AuthTokenStatus): Promise { - if (status !== AuthTokenStatus.Initializing) { - this._status = status; - this._onDidChangeStatus.fire(status); - } - } - -} - -registerSingleton(IAuthTokenService, AuthTokenService); diff --git a/src/vs/workbench/services/authentication/browser/authenticationService.ts b/src/vs/workbench/services/authentication/browser/authenticationService.ts new file mode 100644 index 0000000000..74b732e62a --- /dev/null +++ b/src/vs/workbench/services/authentication/browser/authenticationService.ts @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Session } from 'vs/editor/common/modes'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { MainThreadAuthenticationProvider } from 'vs/workbench/api/browser/mainThreadAuthentication'; + +export const IAuthenticationService = createDecorator('IAuthenticationService'); + +export interface IAuthenticationService { + _serviceBrand: undefined; + + registerAuthenticationProvider(id: string, provider: MainThreadAuthenticationProvider): void; + unregisterAuthenticationProvider(id: string): void; + sessionsUpdate(providerId: string): void; + + readonly onDidRegisterAuthenticationProvider: Event; + readonly onDidUnregisterAuthenticationProvider: Event; + + readonly onDidChangeSessions: Event; + getSessions(providerId: string): Promise | undefined>; + login(providerId: string): Promise; + logout(providerId: string, accountId: string): Promise; +} + +export class AuthenticationService extends Disposable implements IAuthenticationService { + _serviceBrand: undefined; + + private _authenticationProviders: Map = new Map(); + + private _onDidRegisterAuthenticationProvider: Emitter = this._register(new Emitter()); + readonly onDidRegisterAuthenticationProvider: Event = this._onDidRegisterAuthenticationProvider.event; + + private _onDidUnregisterAuthenticationProvider: Emitter = this._register(new Emitter()); + readonly onDidUnregisterAuthenticationProvider: Event = this._onDidUnregisterAuthenticationProvider.event; + + private _onDidChangeSessions: Emitter = this._register(new Emitter()); + readonly onDidChangeSessions: Event = this._onDidChangeSessions.event; + + constructor() { + super(); + } + + registerAuthenticationProvider(id: string, authenticationProvider: MainThreadAuthenticationProvider): void { + this._authenticationProviders.set(id, authenticationProvider); + this._onDidRegisterAuthenticationProvider.fire(id); + } + + unregisterAuthenticationProvider(id: string): void { + this._authenticationProviders.delete(id); + this._onDidUnregisterAuthenticationProvider.fire(id); + } + + sessionsUpdate(id: string): void { + this._onDidChangeSessions.fire(id); + } + + async getSessions(id: string): Promise | undefined> { + const authProvider = this._authenticationProviders.get(id); + if (authProvider) { + return await authProvider.getSessions(); + } + + return undefined; + } + + async login(id: string): Promise { + const authProvider = this._authenticationProviders.get(id); + if (authProvider) { + return authProvider.login(); + } else { + throw new Error(`No authentication provider '${id}' is currently registered.`); + } + } + + async logout(id: string, accountId: string): Promise { + const authProvider = this._authenticationProviders.get(id); + if (authProvider) { + return authProvider.logout(accountId); + } else { + throw new Error(`No authentication provider '${id}' is currently registered.`); + } + } +} + +registerSingleton(IAuthenticationService, AuthenticationService); 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 de7fc64efc..fedc27bbd3 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 @@ -14,7 +14,7 @@ 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 { getRandomTestPath } from 'vs/base/test/node/testUtils'; -import { DefaultEndOfLine } from 'vs/editor/common/model'; +import { DefaultEndOfLine, ITextSnapshot } from 'vs/editor/common/model'; import { Schemas } from 'vs/base/common/network'; import { IWindowConfiguration } from 'vs/platform/windows/common/windows'; import { FileService } from 'vs/platform/files/common/fileService'; @@ -57,6 +57,9 @@ export class NodeTestBackupFileService extends BackupFileService { readonly fileService: IFileService; + private backupResourceJoiners: Function[]; + private discardBackupJoiners: Function[]; + constructor(workspaceBackupPath: string) { const environmentService = new TestBackupEnvironmentService(workspaceBackupPath); const fileService = new FileService(new NullLogService()); @@ -67,11 +70,37 @@ export class NodeTestBackupFileService extends BackupFileService { super(environmentService, fileService); this.fileService = fileService; + this.backupResourceJoiners = []; + this.discardBackupJoiners = []; } toBackupResource(resource: URI): URI { return super.toBackupResource(resource); } + + joinBackupResource(): Promise { + return new Promise(resolve => this.backupResourceJoiners.push(resolve)); + } + + async backupResource(resource: URI, content: ITextSnapshot, versionId?: number, meta?: any): Promise { + await super.backupResource(resource, content, versionId, meta); + + while (this.backupResourceJoiners.length) { + this.backupResourceJoiners.pop()!(); + } + } + + joinDiscardBackup(): Promise { + return new Promise(resolve => this.discardBackupJoiners.push(resolve)); + } + + async discardResourceBackup(resource: URI): Promise { + await super.discardResourceBackup(resource); + + while (this.discardBackupJoiners.length) { + this.discardBackupJoiners.pop()!(); + } + } } suite('BackupFileService', () => { diff --git a/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts b/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts index 98dbc996d6..2069c2d574 100644 --- a/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts +++ b/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { mergeSort } from 'vs/base/common/arrays'; -import { dispose, IDisposable, IReference } from 'vs/base/common/lifecycle'; +import { dispose, IDisposable, IReference, toDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IBulkEditOptions, IBulkEditResult, IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +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 { EndOfLineSequence, IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model'; -import { isResourceFileEdit, isResourceTextEdit, ResourceFileEdit, ResourceTextEdit, WorkspaceEdit } from 'vs/editor/common/modes'; +import { WorkspaceFileEdit, WorkspaceTextEdit, WorkspaceEdit } from 'vs/editor/common/modes'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; import { localize } from 'vs/nls'; @@ -25,25 +25,8 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; +import { Recording } from 'vs/workbench/services/bulkEdit/browser/conflicts'; -abstract class Recording { - - static start(fileService: IFileService): Recording { - - let _changes = new Set(); - let subscription = fileService.onAfterOperation(e => { - _changes.add(e.resource.toString()); - }); - - return { - stop() { return subscription.dispose(); }, - hasChanged(resource) { return _changes.has(resource.toString()); } - }; - } - - abstract stop(): void; - abstract hasChanged(resource: URI): boolean; -} type ValidationResult = { canApply: true } | { canApply: false, reason: URI }; @@ -64,7 +47,7 @@ class ModelEditTask implements IDisposable { this._modelReference.dispose(); } - addEdit(resourceEdit: ResourceTextEdit): void { + addEdit(resourceEdit: WorkspaceTextEdit): void { this._expectedModelVersionId = resourceEdit.modelVersionId; for (const edit of resourceEdit.edits) { if (typeof edit.eol === 'number') { @@ -141,13 +124,13 @@ class EditorEditTask extends ModelEditTask { class BulkEditModel implements IDisposable { - private _edits = new Map(); + private _edits = new Map(); private _tasks: ModelEditTask[] | undefined; constructor( private readonly _editor: ICodeEditor | undefined, private readonly _progress: IProgress, - edits: ResourceTextEdit[], + edits: WorkspaceTextEdit[], @IEditorWorkerService private readonly _editorWorker: IEditorWorkerService, @ITextModelService private readonly _textModelResolverService: ITextModelService, ) { @@ -160,7 +143,7 @@ class BulkEditModel implements IDisposable { } } - private _addEdit(edit: ResourceTextEdit): void { + private _addEdit(edit: WorkspaceTextEdit): void { let array = this._edits.get(edit.resource.toString()); if (!array) { array = []; @@ -198,7 +181,7 @@ class BulkEditModel implements IDisposable { for (const edit of value) { if (makeMinimal) { const newEdits = await this._editorWorker.computeMoreMinimalEdits(edit.resource, edit.edits); - task.addEdit({ ...edit, edits: newEdits! }); + task.addEdit({ ...edit, edits: newEdits ?? edit.edits }); } else { task.addEdit(edit); @@ -234,7 +217,7 @@ class BulkEditModel implements IDisposable { } } -type Edit = ResourceFileEdit | ResourceTextEdit; +type Edit = WorkspaceFileEdit | WorkspaceTextEdit; class BulkEdit { @@ -260,7 +243,7 @@ class BulkEdit { } ariaMessage(): string { - const editCount = this._edits.reduce((prev, cur) => isResourceFileEdit(cur) ? prev : prev + cur.edits.length, 0); + const editCount = this._edits.reduce((prev, cur) => WorkspaceFileEdit.is(cur) ? prev : prev + cur.edits.length, 0); const resourceCount = this._edits.length; if (editCount === 0) { return localize('summary.0', "Made no edits"); @@ -280,15 +263,15 @@ class BulkEdit { let group: Edit[] | undefined; for (const edit of this._edits) { if (!group - || (isResourceFileEdit(group[0]) && !isResourceFileEdit(edit)) - || (isResourceTextEdit(group[0]) && !isResourceTextEdit(edit)) + || (WorkspaceFileEdit.is(group[0]) && !WorkspaceFileEdit.is(edit)) + || (WorkspaceTextEdit.is(group[0]) && !WorkspaceTextEdit.is(edit)) ) { group = []; groups.push(group); } group.push(edit); - if (isResourceFileEdit(edit)) { + if (WorkspaceFileEdit.is(edit)) { total += 1; } else if (!seen.has(edit.resource.toString())) { seen.add(edit.resource.toString()); @@ -304,15 +287,15 @@ class BulkEdit { // do it. for (const group of groups) { - if (isResourceFileEdit(group[0])) { - await this._performFileEdits(group, progress); + if (WorkspaceFileEdit.is(group[0])) { + await this._performFileEdits(group, progress); } else { - await this._performTextEdits(group, progress); + await this._performTextEdits(group, progress); } } } - private async _performFileEdits(edits: ResourceFileEdit[], progress: IProgress) { + private async _performFileEdits(edits: WorkspaceFileEdit[], progress: IProgress) { this._logService.debug('_performFileEdits', JSON.stringify(edits)); for (const edit of edits) { progress.report(undefined); @@ -347,7 +330,7 @@ class BulkEdit { } } - private async _performTextEdits(edits: ResourceTextEdit[], progress: IProgress): Promise { + private async _performTextEdits(edits: WorkspaceTextEdit[], progress: IProgress): Promise { this._logService.debug('_performTextEdits', JSON.stringify(edits)); const recording = Recording.start(this._fileService); @@ -381,6 +364,8 @@ export class BulkEditService implements IBulkEditService { _serviceBrand: undefined; + private _previewHandler?: IBulkEditPreviewHandler; + constructor( @ILogService private readonly _logService: ILogService, @IModelService private readonly _modelService: IModelService, @@ -393,14 +378,31 @@ export class BulkEditService implements IBulkEditService { @IConfigurationService private readonly _configurationService: IConfigurationService ) { } - apply(edit: WorkspaceEdit, options: IBulkEditOptions = {}): Promise { + setPreviewHandler(handler: IBulkEditPreviewHandler): IDisposable { + this._previewHandler = handler; + return toDisposable(() => { + if (this._previewHandler === handler) { + this._previewHandler = undefined; + } + }); + } - let { edits } = edit; - let codeEditor = options.editor; + async apply(edit: WorkspaceEdit, options?: IBulkEditOptions): Promise { + + if (edit.edits.length === 0) { + return { ariaSummary: localize('nothing', "Made no edits") }; + } + + if (this._previewHandler && options?.showPreview) { + edit = await this._previewHandler(edit, options); + } + + const { edits } = edit; + let codeEditor = options?.editor; // First check if loaded models were not changed in the meantime for (const edit of edits) { - if (!isResourceFileEdit(edit) && typeof edit.modelVersionId === 'number') { + if (!WorkspaceFileEdit.is(edit) && typeof edit.modelVersionId === 'number') { let model = this._modelService.getModel(edit.resource); if (model && model.getVersionId() !== edit.modelVersionId) { // model changed in the meantime @@ -423,7 +425,7 @@ export class BulkEditService implements IBulkEditService { codeEditor = undefined; } const bulkEdit = new BulkEdit( - codeEditor, options.progress, edits, + codeEditor, options?.progress, edits, this._logService, this._textModelService, this._fileService, this._workerService, this._textFileService, this._labelService, this._configurationService ); return bulkEdit.perform().then(() => { diff --git a/src/vs/workbench/services/bulkEdit/browser/conflicts.ts b/src/vs/workbench/services/bulkEdit/browser/conflicts.ts new file mode 100644 index 0000000000..d0ff7c9994 --- /dev/null +++ b/src/vs/workbench/services/bulkEdit/browser/conflicts.ts @@ -0,0 +1,118 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IFileService } from 'vs/platform/files/common/files'; +import { URI } from 'vs/base/common/uri'; +import { WorkspaceEdit, WorkspaceTextEdit } from 'vs/editor/common/modes'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { ResourceMap } from 'vs/base/common/map'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Emitter, Event } from 'vs/base/common/event'; +import type { ITextModel } from 'vs/editor/common/model'; + +export abstract class Recording { + + static start(fileService: IFileService): Recording { + + let _changes = new Set(); + let subscription = fileService.onAfterOperation(e => { + _changes.add(e.resource.toString()); + }); + + return { + stop() { return subscription.dispose(); }, + hasChanged(resource) { return _changes.has(resource.toString()); } + }; + } + + abstract stop(): void; + abstract hasChanged(resource: URI): boolean; +} + +export class ConflictDetector { + + private readonly _conflicts = new ResourceMap(); + private readonly _changes = new ResourceMap(); + private readonly _disposables = new DisposableStore(); + + private readonly _onDidConflict = new Emitter(); + readonly onDidConflict: Event = this._onDidConflict.event; + + constructor( + workspaceEdit: WorkspaceEdit, + @IFileService fileService: IFileService, + @IModelService modelService: IModelService, + ) { + + const _workspaceEditResources = new ResourceMap(); + + for (let edit of workspaceEdit.edits) { + if (WorkspaceTextEdit.is(edit)) { + + _workspaceEditResources.set(edit.resource, true); + + if (typeof edit.modelVersionId === 'number') { + const model = modelService.getModel(edit.resource); + if (model && model.getVersionId() !== edit.modelVersionId) { + this._conflicts.set(edit.resource, true); + this._onDidConflict.fire(this); + } + } + + } else if (edit.newUri) { + _workspaceEditResources.set(edit.newUri, true); + + } else if (edit.oldUri) { + _workspaceEditResources.set(edit.oldUri, true); + } + } + + // listen to file changes + this._disposables.add(fileService.onFileChanges(e => { + for (let change of e.changes) { + + // change + this._changes.set(change.resource, true); + + // conflict + if (_workspaceEditResources.has(change.resource)) { + this._conflicts.set(change.resource, true); + this._onDidConflict.fire(this); + } + } + })); + + + // listen to model changes...? + const onDidChangeModel = (model: ITextModel) => { + // change + this._changes.set(model.uri, true); + + // conflict + if (_workspaceEditResources.has(model.uri)) { + this._conflicts.set(model.uri, true); + this._onDidConflict.fire(this); + } + }; + for (let model of modelService.getModels()) { + this._disposables.add(model.onDidChangeContent(() => onDidChangeModel(model))); + } + } + + dispose(): void { + this._disposables.dispose(); + this._onDidConflict.dispose(); + } + + list(): URI[] { + const result: URI[] = this._conflicts.keys(); + this._changes.forEach((_value, key) => { + if (!this._conflicts.has(key)) { + result.push(key); + } + }); + return result; + } +} diff --git a/src/vs/workbench/services/configuration/browser/configuration.ts b/src/vs/workbench/services/configuration/browser/configuration.ts index ead4ad7b60..b42d1397db 100644 --- a/src/vs/workbench/services/configuration/browser/configuration.ts +++ b/src/vs/workbench/services/configuration/browser/configuration.ts @@ -9,7 +9,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import * as errors from 'vs/base/common/errors'; import { Disposable, IDisposable, dispose, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { RunOnceScheduler, runWhenIdle } from 'vs/base/common/async'; -import { FileChangeType, FileChangesEvent, IFileService, whenProviderRegistered } from 'vs/platform/files/common/files'; +import { FileChangeType, FileChangesEvent, IFileService, whenProviderRegistered, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; import { ConfigurationModel, ConfigurationModelParser } from 'vs/platform/configuration/common/configurationModels'; import { WorkspaceConfigurationModelParser, StandaloneConfigurationModelParser } from 'vs/workbench/services/configuration/common/configurationModels'; import { TASKS_CONFIGURATION_KEY, FOLDER_SETTINGS_NAME, LAUNCH_CONFIGURATION_KEY, IConfigurationCache, ConfigurationKey, REMOTE_MACHINE_SCOPES, FOLDER_SCOPES, WORKSPACE_SCOPES } from 'vs/workbench/services/configuration/common/configuration'; @@ -127,8 +127,7 @@ class FileServiceBasedConfigurationWithNames extends Disposable { const content = await this.fileService.readFile(resource); return content.value.toString(); } catch (error) { - const exists = await this.fileService.exists(resource); - if (exists) { + if ((error).fileOperationResult !== FileOperationResult.FILE_NOT_FOUND) { errors.onUnexpectedError(error); } } diff --git a/src/vs/workbench/services/configuration/common/configurationEditingService.ts b/src/vs/workbench/services/configuration/common/configurationEditingService.ts index f0a73994c0..07250c4e9a 100644 --- a/src/vs/workbench/services/configuration/common/configurationEditingService.ts +++ b/src/vs/workbench/services/configuration/common/configurationEditingService.ts @@ -487,7 +487,7 @@ export class ConfigurationEditingService { } // Target cannot be dirty if not writing into buffer - if (checkDirty && this.textFileService.isDirty(operation.resource)) { + if (checkDirty && operation.resource && this.textFileService.isDirty(operation.resource)) { return this.reject(ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_DIRTY, target, operation); } return reference; diff --git a/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts index a7c5be58e6..43baa06b5f 100644 --- a/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts @@ -1094,7 +1094,7 @@ suite.skip('WorkspaceConfigurationService - Folder', () => { // {{SQL CARBON EDI test('no change event when there are no global tasks', async () => { const target = sinon.spy(); testObject.onDidChangeConfiguration(target); - await timeout(500); + await timeout(5); assert.ok(target.notCalled); }); diff --git a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts index 1fc05900fe..84d549e038 100644 --- a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import { IWindowOpenable } from 'vs/platform/windows/common/windows'; -import { IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, FileFilter, IFileDialogService, IDialogService, ConfirmResult, getConfirmMessage } from 'vs/platform/dialogs/common/dialogs'; +import { IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, FileFilter, IFileDialogService, IDialogService, ConfirmResult, getFileNamesMessage } from 'vs/platform/dialogs/common/dialogs'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -21,6 +21,9 @@ import { IFileService } from 'vs/platform/files/common/files'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import Severity from 'vs/base/common/severity'; +import { coalesce } from 'vs/base/common/arrays'; +import { trim } from 'vs/base/common/strings'; +import { IModeService } from 'vs/editor/common/services/modeService'; export abstract class AbstractFileDialogService implements IFileDialogService { @@ -35,7 +38,8 @@ export abstract class AbstractFileDialogService implements IFileDialogService { @IConfigurationService protected readonly configurationService: IConfigurationService, @IFileService protected readonly fileService: IFileService, @IOpenerService protected readonly openerService: IOpenerService, - @IDialogService private readonly dialogService: IDialogService + @IDialogService private readonly dialogService: IDialogService, + @IModeService private readonly modeService: IModeService ) { } defaultFilePath(schemeFilter = this.getSchemeFilterForWindow()): URI | undefined { @@ -90,10 +94,12 @@ export abstract class AbstractFileDialogService implements IFileDialogService { } let message: string; + let detail = nls.localize('saveChangesDetail', "Your changes will be lost if you don't save them."); if (fileNamesOrResources.length === 1) { message = nls.localize('saveChangesMessage', "Do you want to save the changes you made to {0}?", typeof fileNamesOrResources[0] === 'string' ? fileNamesOrResources[0] : resources.basename(fileNamesOrResources[0])); } else { - message = getConfirmMessage(nls.localize('saveChangesMessages', "Do you want to save the changes to the following {0} files?", fileNamesOrResources.length), fileNamesOrResources); + message = nls.localize('saveChangesMessages', "Do you want to save the changes to the following {0} files?", fileNamesOrResources.length); + detail = getFileNamesMessage(fileNamesOrResources) + '\n' + detail; } const buttons: string[] = [ @@ -104,7 +110,7 @@ export abstract class AbstractFileDialogService implements IFileDialogService { const { choice } = await this.dialogService.show(Severity.Warning, message, buttons, { cancelId: 2, - detail: nls.localize('saveChangesDetail', "Your changes will be lost if you don't save them.") + detail }); switch (choice) { @@ -220,7 +226,56 @@ export abstract class AbstractFileDialogService implements IFileDialogService { abstract pickFileAndOpen(options: IPickAndOpenOptions): Promise; abstract pickFolderAndOpen(options: IPickAndOpenOptions): Promise; abstract pickWorkspaceAndOpen(options: IPickAndOpenOptions): Promise; - abstract pickFileToSave(options: ISaveDialogOptions): Promise; abstract showSaveDialog(options: ISaveDialogOptions): Promise; abstract showOpenDialog(options: IOpenDialogOptions): Promise; + + abstract pickFileToSave(defaultUri: URI, availableFileSystems?: string[]): Promise; + + protected getPickFileToSaveDialogOptions(defaultUri: URI, availableFileSystems?: string[]): ISaveDialogOptions { + const options: ISaveDialogOptions = { + defaultUri, + title: nls.localize('saveAsTitle', "Save As"), + availableFileSystems, + }; + + interface IFilter { name: string; extensions: string[]; } + + // Build the file filter by using our known languages + const ext: string | undefined = defaultUri ? resources.extname(defaultUri) : undefined; + let matchingFilter: IFilter | undefined; + const filters: IFilter[] = coalesce(this.modeService.getRegisteredLanguageNames().map(languageName => { + const extensions = this.modeService.getExtensions(languageName); + if (!extensions || !extensions.length) { + return null; + } + + const filter: IFilter = { name: languageName, extensions: extensions.slice(0, 10).map(e => trim(e, '.')) }; + + if (ext && extensions.indexOf(ext) >= 0) { + matchingFilter = filter; + + return null; // matching filter will be added last to the top + } + + return filter; + })); + + // Filters are a bit weird on Windows, based on having a match or not: + // Match: we put the matching filter first so that it shows up selected and the all files last + // No match: we put the all files filter first + const allFilesFilter = { name: nls.localize('allFiles', "All Files"), extensions: ['*'] }; + if (matchingFilter) { + filters.unshift(matchingFilter); + filters.unshift(allFilesFilter); + } else { + filters.unshift(allFilesFilter); + } + + // Allow to save file without extension + filters.push({ name: nls.localize('noExt', "No Extension"), extensions: [''] }); + + options.filters = filters; + + return options; + } } diff --git a/src/vs/workbench/services/dialogs/browser/fileDialogService.ts b/src/vs/workbench/services/dialogs/browser/fileDialogService.ts index 90af2d4466..b412a3ba62 100644 --- a/src/vs/workbench/services/dialogs/browser/fileDialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/fileDialogService.ts @@ -51,9 +51,9 @@ export class FileDialogService extends AbstractFileDialogService implements IFil return this.pickWorkspaceAndOpenSimplified(schema, options); } - async pickFileToSave(options: ISaveDialogOptions): Promise { - const schema = this.getFileSystemSchema(options); - return this.pickFileToSaveSimplified(schema, options); + async pickFileToSave(defaultUri: URI, availableFileSystems?: string[]): Promise { + const schema = this.getFileSystemSchema({ defaultUri, availableFileSystems }); + return this.pickFileToSaveSimplified(schema, this.getPickFileToSaveDialogOptions(defaultUri, availableFileSystems)); } async showSaveDialog(options: ISaveDialogOptions): Promise { diff --git a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts index 054a20bf91..f9ae47594c 100644 --- a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts @@ -35,6 +35,7 @@ import { ICommandHandler } from 'vs/platform/commands/common/commands'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { normalizeDriveLetter } from 'vs/base/common/labels'; import { SaveReason } from 'vs/workbench/common/editor'; +import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService'; export namespace OpenLocalFileCommand { export const ID = 'workbench.action.files.openLocalFile'; @@ -134,6 +135,7 @@ export class SimpleFileDialog { @IModeService private readonly modeService: IModeService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, + @IRemotePathService private readonly remotePathService: IRemotePathService, @IKeybindingService private readonly keybindingService: IKeybindingService, @IContextKeyService contextKeyService: IContextKeyService, ) { @@ -154,8 +156,8 @@ export class SimpleFileDialog { public async showOpenDialog(options: IOpenDialogOptions = {}): Promise { this.scheme = this.getScheme(options.availableFileSystems, options.defaultUri); - this.userHome = await this.getUserHome(); - const newOptions = await this.getOptions(options); + this.userHome = await this.remotePathService.userHome; + const newOptions = this.getOptions(options); if (!newOptions) { return Promise.resolve(undefined); } @@ -165,9 +167,9 @@ export class SimpleFileDialog { public async showSaveDialog(options: ISaveDialogOptions): Promise { this.scheme = this.getScheme(options.availableFileSystems, options.defaultUri); - this.userHome = await this.getUserHome(); + this.userHome = await this.remotePathService.userHome; this.requiresTrailing = true; - const newOptions = await this.getOptions(options, true); + const newOptions = this.getOptions(options, true); if (!newOptions) { return Promise.resolve(undefined); } @@ -229,16 +231,6 @@ export class SimpleFileDialog { return this.remoteAgentEnvironment; } - private async getUserHome(): Promise { - if (this.scheme !== Schemas.file) { - const env = await this.getRemoteAgentEnvironment(); - if (env) { - return env.userHome; - } - } - return URI.from({ scheme: this.scheme, path: this.environmentService.userHome }); - } - private async pickResource(isSave: boolean = false): Promise { this.allowFolderSelection = !!this.options.canSelectFolders; this.allowFileSelection = !!this.options.canSelectFiles; diff --git a/src/vs/workbench/services/dialogs/electron-browser/fileDialogService.ts b/src/vs/workbench/services/dialogs/electron-browser/fileDialogService.ts index 55cfe83ed4..c265a697be 100644 --- a/src/vs/workbench/services/dialogs/electron-browser/fileDialogService.ts +++ b/src/vs/workbench/services/dialogs/electron-browser/fileDialogService.ts @@ -19,6 +19,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IElectronService } from 'vs/platform/electron/node/electron'; import { AbstractFileDialogService } from 'vs/workbench/services/dialogs/browser/abstractFileDialogService'; import { Schemas } from 'vs/base/common/network'; +import { IModeService } from 'vs/editor/common/services/modeService'; export class FileDialogService extends AbstractFileDialogService implements IFileDialogService { @@ -34,9 +35,10 @@ export class FileDialogService extends AbstractFileDialogService implements IFil @IFileService fileService: IFileService, @IOpenerService openerService: IOpenerService, @IElectronService private readonly electronService: IElectronService, - @IDialogService dialogService: IDialogService + @IDialogService dialogService: IDialogService, + @IModeService modeService: IModeService ) { - super(hostService, contextService, historyService, environmentService, instantiationService, configurationService, fileService, openerService, dialogService); + super(hostService, contextService, historyService, environmentService, instantiationService, configurationService, fileService, openerService, dialogService, modeService); } private toNativeOpenDialogOptions(options: IPickAndOpenOptions): INativeOpenDialogOptions { @@ -107,8 +109,9 @@ export class FileDialogService extends AbstractFileDialogService implements IFil return this.electronService.pickWorkspaceAndOpen(this.toNativeOpenDialogOptions(options)); } - async pickFileToSave(options: ISaveDialogOptions): Promise { - const schema = this.getFileSystemSchema(options); + async pickFileToSave(defaultUri: URI, availableFileSystems?: string[]): Promise { + const schema = this.getFileSystemSchema({ defaultUri, availableFileSystems }); + const options = this.getPickFileToSaveDialogOptions(defaultUri, availableFileSystems); if (this.shouldUseSimplified(schema).useSimplified) { return this.pickFileToSaveSimplified(schema, options); } else { diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index 625e4d9e15..9de328b1b0 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -61,7 +61,6 @@ export class EditorService extends Disposable implements EditorServiceImpl { private readonly openEditorHandlers: IOpenEditorOverrideHandler[] = []; private lastActiveEditor: IEditorInput | undefined = undefined; - private lastActiveGroupId: GroupIdentifier | undefined = undefined; private readonly editorsObserver = this._register(this.instantiationService.createInstance(EditorsObserver)); @@ -110,10 +109,6 @@ export class EditorService extends Disposable implements EditorServiceImpl { return; // ignore if we still have no active editor } - if (this.lastActiveGroupId === group.id && this.lastActiveEditor === group.activeEditor) { - return; // ignore if the editor actually did not change - } - this.doHandleActiveEditorChangeEvent(); } @@ -121,7 +116,6 @@ export class EditorService extends Disposable implements EditorServiceImpl { // Remember as last active const activeGroup = this.editorGroupService.activeGroup; - this.lastActiveGroupId = activeGroup.id; this.lastActiveEditor = withNullAsUndefined(activeGroup.activeEditor); // Fire event to outside parties @@ -186,6 +180,19 @@ export class EditorService extends Disposable implements EditorServiceImpl { return undefined; } + get activeTextEditorMode(): string | undefined { + let activeCodeEditor: ICodeEditor | undefined = undefined; + + const activeTextEditorWidget = this.activeTextEditorWidget; + if (isDiffEditor(activeTextEditorWidget)) { + activeCodeEditor = activeTextEditorWidget.getModifiedEditor(); + } else { + activeCodeEditor = activeTextEditorWidget; + } + + return activeCodeEditor?.getModel()?.getLanguageIdentifier().language; + } + get count(): number { return this.editorsObserver.count; } @@ -603,7 +610,21 @@ export class EditorService extends Disposable implements EditorServiceImpl { // Untitled file support const untitledInput = input as IUntitledTextResourceInput; if (untitledInput.forceUntitled || !untitledInput.resource || (untitledInput.resource && untitledInput.resource.scheme === Schemas.untitled)) { - return this.untitledTextEditorService.createOrGet(untitledInput.resource, untitledInput.mode, untitledInput.contents, untitledInput.encoding); + const untitledOptions = { + mode: untitledInput.mode, + initialValue: untitledInput.contents, + encoding: untitledInput.encoding + }; + + // Untitled resource: use as hint for an existing untitled editor + if (untitledInput.resource?.scheme === Schemas.untitled) { + return this.untitledTextEditorService.create({ untitledResource: untitledInput.resource, ...untitledOptions }); + } + + // Other resource: use as hint for associated filepath + else { + return this.untitledTextEditorService.create({ associatedResource: untitledInput.resource, ...untitledOptions }); + } } // Resource Editor Support @@ -839,6 +860,7 @@ export class DelegatingEditorService implements IEditorService { get activeEditor(): IEditorInput | undefined { return this.editorService.activeEditor; } get activeControl(): IVisibleEditor | undefined { return this.editorService.activeControl; } get activeTextEditorWidget(): ICodeEditor | IDiffEditor | undefined { return this.editorService.activeTextEditorWidget; } + get activeTextEditorMode(): string | undefined { return this.editorService.activeTextEditorMode; } get visibleEditors(): ReadonlyArray { return this.editorService.visibleEditors; } get visibleControls(): ReadonlyArray { return this.editorService.visibleControls; } get visibleTextEditorWidgets(): ReadonlyArray { return this.editorService.visibleTextEditorWidgets; } diff --git a/src/vs/workbench/services/editor/common/editorService.ts b/src/vs/workbench/services/editor/common/editorService.ts index bb458b1522..b238ad8174 100644 --- a/src/vs/workbench/services/editor/common/editorService.ts +++ b/src/vs/workbench/services/editor/common/editorService.ts @@ -105,6 +105,13 @@ export interface IEditorService { */ readonly activeTextEditorWidget: ICodeEditor | IDiffEditor | undefined; + /** + * The currently active text editor mode or `undefined` if there is currently no active + * editor or the active editor widget is neither a text nor a diff editor. If the active + * editor is a diff editor, the modified side's mode will be taken. + */ + readonly activeTextEditorMode: string | undefined; + /** * All editors that are currently visible. An editor is visible when it is opened in an * editor group and active in that group. Multiple editor groups can be opened at the same time. 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 058afe021d..2cec683cb1 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -14,14 +14,12 @@ 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 { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; 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/common/editor/untitledTextEditorInput'; -import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor'; import { timeout } from 'vs/base/common/async'; import { toResource } from 'vs/base/test/common/utils'; import { IFileService } from 'vs/platform/files/common/files'; @@ -31,7 +29,8 @@ import { UntitledTextEditorModel } from 'vs/workbench/common/editor/untitledText 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 'vscode'; +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'; @@ -73,19 +72,19 @@ class TestEditorInput extends EditorInput implements IFileEditorInput { setFailToOpen(): void { this.fails = true; } - save(groupId: GroupIdentifier, options?: ISaveOptions): Promise { + async save(groupId: GroupIdentifier, options?: ISaveOptions): Promise { this.gotSaved = true; - return Promise.resolve(true); + return true; } - saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise { + async saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise { this.gotSavedAs = true; - return Promise.resolve(true); + return true; } - revert(options?: IRevertOptions): Promise { + async revert(options?: IRevertOptions): Promise { this.gotReverted = true; this.gotSaved = false; this.gotSavedAs = false; - return Promise.resolve(true); + return true; } isDirty(): boolean { return this.dirty; @@ -120,19 +119,26 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite disposables = []; }); - test('basics', async () => { - const partInstantiator = workbenchInstantiationService(); + function createEditorService(): [EditorPart, EditorService, IInstantiationService] { + const instantiationService = workbenchInstantiationService(); - const part = partInstantiator.createInstance(EditorPart); + const part = instantiationService.createInstance(EditorPart); part.create(document.createElement('div')); part.layout(400, 300); - const testInstantiationService = partInstantiator.createChild(new ServiceCollection([IEditorGroupsService, part])); + instantiationService.stub(IEditorGroupsService, part); - const service: EditorServiceImpl = testInstantiationService.createInstance(EditorService); + const editorService = instantiationService.createInstance(EditorService); + instantiationService.stub(IEditorService, editorService); - const input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-basics')); - const otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-basics')); + return [part, editorService, instantiationService]; + } + + test('basics', async () => { + const [part, service, testInstantiationService] = createEditorService(); + + let input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-basics')); + let otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-basics')); let activeEditorChangeEventCounter = 0; const activeEditorChangeListener = service.onDidActiveEditorChange(() => { @@ -163,6 +169,7 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite assert.equal(service.visibleControls.length, 1); assert.equal(service.visibleControls[0], editor); assert.ok(!service.activeTextEditorWidget); + assert.ok(!service.activeTextEditorMode); assert.equal(service.visibleTextEditorWidgets.length, 0); assert.equal(service.isOpen(input), true); assert.equal(service.getOpened({ resource: input.getResource() }), input); @@ -180,7 +187,14 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite assert.equal(visibleEditorChangeEventCounter, 2); assert.ok(input.gotDisposed); - // Open again 2 inputs + // Open again 2 inputs (disposed editors are ignored!) + await service.openEditor(input, { pinned: true }); + 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')); + await service.openEditor(input, { pinned: true }); editor = await service.openEditor(otherInput, { pinned: true }); @@ -204,15 +218,7 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite }); test('openEditors() / replaceEditors()', async () => { - const partInstantiator = workbenchInstantiationService(); - - const part = partInstantiator.createInstance(EditorPart); - part.create(document.createElement('div')); - part.layout(400, 300); - - const testInstantiationService = partInstantiator.createChild(new ServiceCollection([IEditorGroupsService, part])); - - const service: IEditorService = testInstantiationService.createInstance(EditorService); + const [part, service, testInstantiationService] = createEditorService(); const input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-openEditors')); const otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-openEditors')); @@ -234,7 +240,7 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite test('caching', function () { const instantiationService = workbenchInstantiationService(); - const service: EditorService = instantiationService.createInstance(EditorService); + const service = instantiationService.createInstance(EditorService); // Cached Input (Files) const fileResource1 = toResource.call(this, '/foo/bar/cache1.js'); @@ -283,7 +289,7 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite test('createInput', async function () { const instantiationService = workbenchInstantiationService(); - const service: EditorService = instantiationService.createInstance(EditorService); + const service = instantiationService.createInstance(EditorService); const mode = 'create-input-test'; ModesRegistry.registerLanguage({ @@ -375,27 +381,19 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite const ed = instantiationService.createInstance(MyEditor, 'my.editor'); const inp = instantiationService.createInstance(ResourceEditorInput, 'name', 'description', URI.parse('my://resource-delegate'), undefined); - const delegate = instantiationService.createInstance(DelegatingEditorService, (delegate, group, input) => { + const delegate = instantiationService.createInstance(DelegatingEditorService, async (delegate, group, input) => { assert.strictEqual(input, inp); done(); - return Promise.resolve(ed); + return ed; }); delegate.openEditor(inp); }); test('close editor does not dispose when editor opened in other group', async () => { - const partInstantiator = workbenchInstantiationService(); - - const part = partInstantiator.createInstance(EditorPart); - part.create(document.createElement('div')); - part.layout(400, 300); - - const testInstantiationService = partInstantiator.createChild(new ServiceCollection([IEditorGroupsService, part])); - - const service: IEditorService = testInstantiationService.createInstance(EditorService); + const [part, service, testInstantiationService] = createEditorService(); const input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-close1')); @@ -424,15 +422,7 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite }); test('open to the side', async () => { - const partInstantiator = workbenchInstantiationService(); - - const part = partInstantiator.createInstance(EditorPart); - part.create(document.createElement('div')); - part.layout(400, 300); - - const testInstantiationService = partInstantiator.createChild(new ServiceCollection([IEditorGroupsService, part])); - - const service: IEditorService = testInstantiationService.createInstance(EditorService); + const [part, service, testInstantiationService] = createEditorService(); const input1 = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource1-openside')); const input2 = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-openside')); @@ -458,15 +448,7 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite }); test('editor group activation', async () => { - const partInstantiator = workbenchInstantiationService(); - - const part = partInstantiator.createInstance(EditorPart); - part.create(document.createElement('div')); - part.layout(400, 300); - - const testInstantiationService = partInstantiator.createChild(new ServiceCollection([IEditorGroupsService, part])); - - const service: IEditorService = testInstantiationService.createInstance(EditorService); + const [part, service, testInstantiationService] = createEditorService(); const input1 = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource1-openside')); const input2 = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-openside')); @@ -501,18 +483,10 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite }); test('active editor change / visible editor change events', async function () { - const partInstantiator = workbenchInstantiationService(); + const [part, service, testInstantiationService] = createEditorService(); - const part = partInstantiator.createInstance(EditorPart); - part.create(document.createElement('div')); - part.layout(400, 300); - - const testInstantiationService = partInstantiator.createChild(new ServiceCollection([IEditorGroupsService, part])); - - const service: EditorServiceImpl = testInstantiationService.createInstance(EditorService); - - const input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-active')); - const otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-active')); + let input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-active')); + let otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-active')); let activeEditorChangeEventFired = false; const activeEditorChangeListener = service.onDidActiveEditorChange(() => { @@ -563,7 +537,9 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); - // 2.) open, open same (forced open) + // 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')); editor = await service.openEditor(input); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); @@ -574,7 +550,9 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite await closeEditorAndWaitForNextToOpen(group, input); - // 3.) open, open inactive, close + // 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')); editor = await service.openEditor(input, { pinned: true }); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); @@ -587,7 +565,9 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); - // 4.) open, open inactive, close inactive + // 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')); editor = await service.openEditor(input, { pinned: true }); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); @@ -604,7 +584,9 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); - // 5.) add group, remove group + // 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')); editor = await service.openEditor(input, { pinned: true }); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); @@ -625,7 +607,9 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); - // 6.) open editor in inactive group + // 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')); editor = await service.openEditor(input, { pinned: true }); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); @@ -646,7 +630,9 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); - // 7.) activate group + // 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')); editor = await service.openEditor(input, { pinned: true }); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); @@ -671,7 +657,9 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); - // 8.) move editor + // 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')); editor = await service.openEditor(input, { pinned: true }); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); @@ -688,7 +676,9 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); - // 9.) close editor in inactive group + // 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')); editor = await service.openEditor(input, { pinned: true }); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); @@ -712,16 +702,60 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite part.dispose(); }); + test('two active editor change events when opening editor to the side', async function () { + const [part, service, testInstantiationService] = createEditorService(); + + let input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-active')); + + let activeEditorChangeEvents = 0; + const activeEditorChangeListener = service.onDidActiveEditorChange(() => { + activeEditorChangeEvents++; + }); + + function assertActiveEditorChangedEvent(expected: number) { + assert.equal(activeEditorChangeEvents, expected, `Unexpected active editor change state (got ${activeEditorChangeEvents}, expected ${expected})`); + activeEditorChangeEvents = 0; + } + + await part.whenRestored; + + await service.openEditor(input, { pinned: true }); + assertActiveEditorChangedEvent(1); + + await service.openEditor(input, { pinned: true }, SIDE_GROUP); + + // we expect 2 active editor change events: one for the fact that the + // active editor is now in the side group but also one for when the + // editor has finished loading. we used to ignore that second change + // event, however many listeners are interested on the active editor + // when it has fully loaded (e.g. a model is set). as such, we cannot + // simply ignore that second event from the editor service, even though + // the actual editor input is the same + assertActiveEditorChangedEvent(2); + + // cleanup + activeEditorChangeListener.dispose(); + + part.dispose(); + }); + + test('activeTextEditorWidget / activeTextEditorMode', async () => { + const [part, service] = createEditorService(); + + await part.whenRestored; + + // Open untitled input + let editor = await service.openEditor({}); + + assert.equal(service.activeControl, editor); + assert.equal(service.activeTextEditorWidget, editor?.getControl()); + assert.equal(service.activeTextEditorMode, 'plaintext'); + + part.dispose(); + }); + test('openEditor returns NULL when opening fails or is inactive', async function () { - const partInstantiator = workbenchInstantiationService(); - - const part = partInstantiator.createInstance(EditorPart); - part.create(document.createElement('div')); - part.layout(400, 300); - - const testInstantiationService = partInstantiator.createChild(new ServiceCollection([IEditorGroupsService, part])); - - const service: EditorServiceImpl = testInstantiationService.createInstance(EditorService); + const [part, service, testInstantiationService] = createEditorService(); const input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-active')); const otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-inactive')); @@ -743,15 +777,7 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite }); test('save, saveAll, revertAll', async function () { - const partInstantiator = workbenchInstantiationService(); - - const part = partInstantiator.createInstance(EditorPart); - part.create(document.createElement('div')); - part.layout(400, 300); - - const testInstantiationService = partInstantiator.createChild(new ServiceCollection([IEditorGroupsService, part])); - - const service: IEditorService = testInstantiationService.createInstance(EditorService); + const [part, service, testInstantiationService] = createEditorService(); const input1 = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource1-openside')); input1.dirty = true; 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 d1b31720eb..5882322f89 100644 --- a/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts @@ -106,9 +106,9 @@ suite.skip('EditorsObserver', function () { //{{SQL CARBON EDIT}} disable failin disposables = []; }); - - test('basics (single group)', async () => { + async function createPart(): Promise { const instantiationService = workbenchInstantiationService(); + instantiationService.invokeFunction(accessor => Registry.as(EditorExtensions.EditorInputFactories).start(accessor)); const part = instantiationService.createInstance(EditorPart); part.create(document.createElement('div')); @@ -116,8 +116,20 @@ suite.skip('EditorsObserver', function () { //{{SQL CARBON EDIT}} disable failin await part.whenRestored; + return part; + } + + async function createEditorObserver(): Promise<[EditorPart, EditorsObserver]> { + const part = await createPart(); + const observer = new EditorsObserver(part, new TestStorageService()); + return [part, observer]; + } + + test('basics (single group)', async () => { + const [part, observer] = await createEditorObserver(); + let observerChangeListenerCalled = false; const listener = observer.onDidChange(() => { observerChangeListenerCalled = true; @@ -183,18 +195,10 @@ suite.skip('EditorsObserver', function () { //{{SQL CARBON EDIT}} disable failin }); test('basics (multi group)', async () => { - const instantiationService = workbenchInstantiationService(); - - const part = instantiationService.createInstance(EditorPart); - part.create(document.createElement('div')); - part.layout(400, 300); - - await part.whenRestored; + const [part, observer] = await createEditorObserver(); const rootGroup = part.activeGroup; - const observer = new EditorsObserver(part, new TestStorageService()); - let currentEditorsMRU = observer.editors; assert.equal(currentEditorsMRU.length, 0); @@ -252,15 +256,7 @@ suite.skip('EditorsObserver', function () { //{{SQL CARBON EDIT}} disable failin }); test('copy group', async () => { - const instantiationService = workbenchInstantiationService(); - - const part = instantiationService.createInstance(EditorPart); - part.create(document.createElement('div')); - part.layout(400, 300); - - await part.whenRestored; - - const observer = new EditorsObserver(part, new TestStorageService()); + const [part, observer] = await createEditorObserver(); const input1 = new EditorsObserverTestEditorInput(URI.parse('foo://bar1')); const input2 = new EditorsObserverTestEditorInput(URI.parse('foo://bar2')); @@ -303,14 +299,7 @@ suite.skip('EditorsObserver', function () { //{{SQL CARBON EDIT}} disable failin }); test('initial editors are part of observer and state is persisted & restored (single group)', async () => { - const instantiationService = workbenchInstantiationService(); - instantiationService.invokeFunction(accessor => Registry.as(EditorExtensions.EditorInputFactories).start(accessor)); - - const part = instantiationService.createInstance(EditorPart); - part.create(document.createElement('div')); - part.layout(400, 300); - - await part.whenRestored; + const part = await createPart(); const rootGroup = part.activeGroup; @@ -353,13 +342,7 @@ suite.skip('EditorsObserver', function () { //{{SQL CARBON EDIT}} disable failin }); test('initial editors are part of observer (multi group)', async () => { - const instantiationService = workbenchInstantiationService(); - - const part = instantiationService.createInstance(EditorPart); - part.create(document.createElement('div')); - part.layout(400, 300); - - await part.whenRestored; + const part = await createPart(); const rootGroup = part.activeGroup; @@ -404,14 +387,7 @@ suite.skip('EditorsObserver', function () { //{{SQL CARBON EDIT}} disable failin }); test('observer does not restore editors that cannot be serialized', async () => { - const instantiationService = workbenchInstantiationService(); - instantiationService.invokeFunction(accessor => Registry.as(EditorExtensions.EditorInputFactories).start(accessor)); - - const part = instantiationService.createInstance(EditorPart); - part.create(document.createElement('div')); - part.layout(400, 300); - - await part.whenRestored; + const part = await createPart(); const rootGroup = part.activeGroup; @@ -440,18 +416,9 @@ suite.skip('EditorsObserver', function () { //{{SQL CARBON EDIT}} disable failin }); test('observer closes editors when limit reached (across all groups)', async () => { - const instantiationService = workbenchInstantiationService(); - - instantiationService.invokeFunction(accessor => Registry.as(EditorExtensions.EditorInputFactories).start(accessor)); - - const part = instantiationService.createInstance(EditorPart); - part.create(document.createElement('div')); - part.layout(400, 300); - + const part = await createPart(); part.enforcePartOptions({ limit: { enabled: true, value: 3 } }); - await part.whenRestored; - const storage = new TestStorageService(); const observer = new EditorsObserver(part, storage); @@ -501,18 +468,9 @@ suite.skip('EditorsObserver', function () { //{{SQL CARBON EDIT}} disable failin }); test('observer closes editors when limit reached (in group)', async () => { - const instantiationService = workbenchInstantiationService(); - - instantiationService.invokeFunction(accessor => Registry.as(EditorExtensions.EditorInputFactories).start(accessor)); - - const part = instantiationService.createInstance(EditorPart); - part.create(document.createElement('div')); - part.layout(400, 300); - + const part = await createPart(); part.enforcePartOptions({ limit: { enabled: true, value: 3, perEditorGroup: true } }); - await part.whenRestored; - const storage = new TestStorageService(); const observer = new EditorsObserver(part, storage); diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index e1026fccfa..4210fa9906 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -133,6 +133,9 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment @memoize get settingsResource(): URI { return joinPath(this.userRoamingDataHome, 'settings.json'); } + @memoize + get argvResource(): URI { return joinPath(this.userRoamingDataHome, 'argv.json'); } + @memoize get settingsSyncPreviewResource(): URI { return joinPath(this.userRoamingDataHome, '.settings.json'); } @@ -235,8 +238,6 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment nodeCachedDataDir?: string; - argvResource!: URI; - disableCrashReporter!: boolean; driverHandle?: string; diff --git a/src/vs/workbench/services/extensions/common/extensionDevOptions.ts b/src/vs/workbench/services/extensions/common/extensionDevOptions.ts index acddff78c0..eaec097050 100644 --- a/src/vs/workbench/services/extensions/common/extensionDevOptions.ts +++ b/src/vs/workbench/services/extensions/common/extensionDevOptions.ts @@ -34,6 +34,6 @@ export function parseExtensionDevOptions(environmentService: IEnvironmentService isExtensionDevHost, isExtensionDevDebug, isExtensionDevDebugBrk, - isExtensionDevTestFromCli, + isExtensionDevTestFromCli }; } diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts index e89488a90c..151105bbb0 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts @@ -199,7 +199,7 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter { onDebouncedOutput(output => { const inspectorUrlMatch = output.data && output.data.match(/ws:\/\/([^\s]+:(\d+)\/[^\s]+)/); if (inspectorUrlMatch) { - if (!this._environmentService.isBuilt) { + if (!this._environmentService.isBuilt && !this._isExtensionDevTestFromCli) { console.log(`%c[Extension Host] %cdebugger inspector at chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=${inspectorUrlMatch[1]}`, 'color: blue', 'color:'); } if (!this._inspectPort) { @@ -207,9 +207,11 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter { this._onDidSetInspectPort.fire(); } } else { - console.group('Extension Host'); - console.log(output.data, ...output.format); - console.groupEnd(); + if (!this._isExtensionDevTestFromCli) { + console.group('Extension Host'); + console.log(output.data, ...output.format); + console.groupEnd(); + } } }); @@ -292,22 +294,22 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter { const expected = this._environmentService.debugExtensionHost.port; const port = await findFreePort(expected, 10 /* try 10 ports */, 5000 /* try up to 5 seconds */); - if (!port) { - console.warn('%c[Extension Host] %cCould not find a free port for debugging', 'color: blue', 'color:'); - return 0; + if (!this._isExtensionDevTestFromCli) { + if (!port) { + console.warn('%c[Extension Host] %cCould not find a free port for debugging', 'color: blue', 'color:'); + } else { + if (port !== expected) { + console.warn(`%c[Extension Host] %cProvided debugging port ${expected} is not free, using ${port} instead.`, 'color: blue', 'color:'); + } + if (this._isExtensionDevDebugBrk) { + console.warn(`%c[Extension Host] %cSTOPPED on first line for debugging on port ${port}`, 'color: blue', 'color:'); + } else { + console.info(`%c[Extension Host] %cdebugger listening on port ${port}`, 'color: blue', 'color:'); + } + } } - if (port !== expected) { - console.warn(`%c[Extension Host] %cProvided debugging port ${expected} is not free, using ${port} instead.`, 'color: blue', 'color:'); - } - if (this._isExtensionDevDebugBrk) { - console.warn(`%c[Extension Host] %cSTOPPED on first line for debugging on port ${port}`, 'color: blue', 'color:'); - } else { - console.info(`%c[Extension Host] %cdebugger listening on port ${port}`, 'color: blue', 'color:'); - } - return port; - - + return port || 0; } private _tryExtHostHandshake(): Promise { diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index 55d86a2a9d..96c9072858 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -478,7 +478,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten // set the resolved authority this._remoteAuthorityResolverService.setResolvedAuthority(resolvedAuthority.authority, resolvedAuthority.options); - this._remoteExplorerService.addEnvironmentTunnels(resolvedAuthority.tunnelInformation?.environmentTunnels); + this._remoteExplorerService.setTunnelInformation(resolvedAuthority.tunnelInformation); // monitor for breakage const connection = this._remoteAgentService.getConnection(); diff --git a/src/vs/workbench/services/extensions/test/node/rpcProtocol.test.ts b/src/vs/workbench/services/extensions/test/node/rpcProtocol.test.ts index 94b7a355e4..8996018460 100644 --- a/src/vs/workbench/services/extensions/test/node/rpcProtocol.test.ts +++ b/src/vs/workbench/services/extensions/test/node/rpcProtocol.test.ts @@ -190,6 +190,7 @@ suite('RPCProtocol', () => { test('issue #72798: null errors are hard to digest', function (done) { delegate = (a1: number, a2: number) => { + // eslint-disable-next-line no-throw-literal throw { 'what': 'what' }; }; bProxy.$m(4, 1).then((res) => { diff --git a/src/vs/workbench/services/extensions/worker/extHost.services.ts b/src/vs/workbench/services/extensions/worker/extHost.services.ts index d4e5e1b9cb..83838caf74 100644 --- a/src/vs/workbench/services/extensions/worker/extHost.services.ts +++ b/src/vs/workbench/services/extensions/worker/extHost.services.ts @@ -22,9 +22,11 @@ import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiatio import { ILogService } from 'vs/platform/log/common/log'; import { ExtHostLogService } from 'vs/workbench/api/worker/extHostLogService'; import { IExtHostTunnelService, ExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService'; +import { IExtHostApiDeprecationService, ExtHostApiDeprecationService, } from 'vs/workbench/api/common/extHostApiDeprecationService'; // register singleton services registerSingleton(ILogService, ExtHostLogService); +registerSingleton(IExtHostApiDeprecationService, ExtHostApiDeprecationService); registerSingleton(IExtHostOutputService, ExtHostOutputService); registerSingleton(IExtHostWorkspace, ExtHostWorkspace); registerSingleton(IExtHostDecorations, ExtHostDecorations); diff --git a/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts b/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts index 2235b4ca9e..d1367cb193 100644 --- a/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts +++ b/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts @@ -14,6 +14,7 @@ import { isUndefinedOrNull } from 'vs/base/common/types'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { equals } from 'vs/base/common/objects'; import { URI } from 'vs/base/common/uri'; +import { isWeb } from 'vs/base/common/platform'; export const AutoSaveAfterShortDelayContext = new RawContextKey('autoSaveAfterShortDelayContext', false); @@ -41,10 +42,10 @@ export interface IFilesConfigurationService { readonly onAutoSaveConfigurationChange: Event; - getAutoSaveMode(): AutoSaveMode; - getAutoSaveConfiguration(): IAutoSaveConfiguration; + getAutoSaveMode(): AutoSaveMode; + toggleAutoSave(): Promise; //#endregion @@ -55,13 +56,15 @@ export interface IFilesConfigurationService { readonly hotExitConfiguration: string | undefined; - preventSaveConflicts(resource: URI): boolean; + preventSaveConflicts(resource: URI, language: string): boolean; } export class FilesConfigurationService extends Disposable implements IFilesConfigurationService { _serviceBrand: undefined; + private static DEFAULT_AUTO_SAVE_MODE = isWeb ? AutoSaveConfiguration.AFTER_DELAY : AutoSaveConfiguration.OFF; + private readonly _onAutoSaveConfigurationChange = this._register(new Emitter()); readonly onAutoSaveConfigurationChange = this._onAutoSaveConfigurationChange.event; @@ -110,7 +113,7 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi protected onFilesConfigurationChange(configuration: IFilesConfiguration): void { // Auto Save - const autoSaveMode = configuration?.files?.autoSave || AutoSaveConfiguration.OFF; + const autoSaveMode = configuration?.files?.autoSave || FilesConfigurationService.DEFAULT_AUTO_SAVE_MODE; switch (autoSaveMode) { case AutoSaveConfiguration.AFTER_DELAY: this.configuredAutoSaveDelay = configuration?.files?.autoSaveDelay; @@ -207,8 +210,8 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi return this.currentHotExitConfig; } - preventSaveConflicts(resource: URI): boolean { - return this.configurationService.getValue('files.preventSaveConflicts', { resource }); + preventSaveConflicts(resource: URI, language: string): boolean { + return this.configurationService.getValue('files.preventSaveConflicts', { resource, overrideIdentifier: language }); } } diff --git a/src/vs/workbench/services/history/browser/history.ts b/src/vs/workbench/services/history/browser/history.ts index 0e2ff7ce8f..b37e19f7d2 100644 --- a/src/vs/workbench/services/history/browser/history.ts +++ b/src/vs/workbench/services/history/browser/history.ts @@ -354,15 +354,11 @@ export class HistoryService extends Disposable implements IHistoryService { private doNavigate(location: IStackEntry): Promise { const options: ITextEditorOptions = { - revealIfOpened: true // support to navigate across editor groups + revealIfOpened: true, // support to navigate across editor groups, + selection: location.selection, + revealInCenterIfOutsideViewport: !!location.selection }; - // Support selection and minimize scrolling by setting revealInCenterIfOutsideViewport - if (location.selection) { - options.selection = location.selection; - options.revealInCenterIfOutsideViewport = true; - } - if (location.input instanceof EditorInput) { return this.editorService.openEditor(location.input, options); } diff --git a/src/vs/workbench/services/keybinding/browser/keybindingService.ts b/src/vs/workbench/services/keybinding/browser/keybindingService.ts index 2df0ae1984..fb842606bd 100644 --- a/src/vs/workbench/services/keybinding/browser/keybindingService.ts +++ b/src/vs/workbench/services/keybinding/browser/keybindingService.ts @@ -44,7 +44,7 @@ import { IKeymapService } from 'vs/workbench/services/keybinding/common/keymapIn import { getDispatchConfig } from 'vs/workbench/services/keybinding/common/dispatchConfig'; import { isArray } from 'vs/base/common/types'; import { INavigatorWithKeyboard, IKeyboard } from 'vs/workbench/services/keybinding/browser/navigatorKeyboard'; -import { ScanCodeUtils, IMMUTABLE_CODE_TO_KEY_CODE } from 'vs/base/common/scanCode'; +import { ScanCode, ScanCodeUtils, IMMUTABLE_CODE_TO_KEY_CODE } from 'vs/base/common/scanCode'; import { flatten } from 'vs/base/common/arrays'; import { BrowserFeatures, KeyboardSupport } from 'vs/base/browser/canIUse'; @@ -143,6 +143,24 @@ const keybindingsExtPoint = ExtensionsRegistry.registerExtensionPoint { fileService.registerProvider(Schemas.file, diskFileSystemProvider); fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, diskFileSystemProvider, environmentService)); instantiationService.stub(IFileService, fileService); - instantiationService.stub(IUntitledTextEditorService, instantiationService.createInstance(UntitledTextEditorService)); instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService)); instantiationService.stub(ITextModelService, instantiationService.createInstance(TextModelResolverService)); instantiationService.stub(IBackupFileService, new TestBackupFileService()); diff --git a/src/vs/workbench/services/path/common/remotePathService.ts b/src/vs/workbench/services/path/common/remotePathService.ts index 3054239277..2b94327540 100644 --- a/src/vs/workbench/services/path/common/remotePathService.ts +++ b/src/vs/workbench/services/path/common/remotePathService.ts @@ -9,6 +9,8 @@ import { URI } from 'vs/base/common/uri'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { Schemas } from 'vs/base/common/network'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; const REMOTE_PATH_SERVICE_ID = 'remotePath'; export const IRemotePathService = createDecorator(REMOTE_PATH_SERVICE_ID); @@ -16,8 +18,10 @@ export const IRemotePathService = createDecorator(REMOTE_PAT export interface IRemotePathService { _serviceBrand: undefined; - path: Promise; + readonly path: Promise; fileURI(path: string): Promise; + + readonly userHome: Promise; } /** @@ -29,7 +33,8 @@ export class RemotePathService implements IRemotePathService { private _extHostOS: Promise; constructor( - @IRemoteAgentService readonly remoteAgentService: IRemoteAgentService + @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService ) { this._extHostOS = remoteAgentService.getEnvironment().then(remoteEnvironment => { return remoteEnvironment ? remoteEnvironment.os : platform.OS; @@ -76,6 +81,19 @@ export class RemotePathService implements IRemotePathService { fragment: '' }); } + + get userHome(): Promise { + return this.remoteAgentService.getEnvironment().then(env => { + + // remote: use remote environment userHome + if (env) { + return env.userHome; + } + + // local: use the userHome from environment + return URI.from({ scheme: Schemas.file, path: this.environmentService.userHome }); + }); + } } registerSingleton(IRemotePathService, RemotePathService, true); diff --git a/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts b/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts index 749ecc4d8d..d66f110b5c 100644 --- a/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts +++ b/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts @@ -13,6 +13,8 @@ import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorIn import { KeybindingsEditorModel } from 'vs/workbench/services/preferences/common/keybindingsEditorModel'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { Settings2EditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export class PreferencesEditorInput extends SideBySideEditorInput { static readonly ID: string = 'workbench.editorinputs.preferencesEditorInput'; @@ -28,10 +30,13 @@ export class PreferencesEditorInput extends SideBySideEditorInput { export class DefaultPreferencesEditorInput extends ResourceEditorInput { static readonly ID = 'workbench.editorinputs.defaultpreferences'; - constructor(defaultSettingsResource: URI, - @ITextModelService textModelResolverService: ITextModelService + constructor( + defaultSettingsResource: URI, + @ITextModelService textModelResolverService: ITextModelService, + @ITextFileService textFileService: ITextFileService, + @IEditorService editorService: IEditorService ) { - super(nls.localize('settingsEditorName', "Default Settings"), '', defaultSettingsResource, undefined, textModelResolverService); + super(nls.localize('settingsEditorName', "Default Settings"), '', defaultSettingsResource, undefined, textModelResolverService, textFileService, editorService); } getTypeId(): string { diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index 26cc44e552..398098b646 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -146,10 +146,7 @@ export class ProgressService extends Disposable implements IProgressService { private withNotificationProgress

, R = unknown>(options: IProgressNotificationOptions, callback: (progress: IProgress<{ message?: string, increment?: number }>) => P, onDidCancel?: (choice?: number) => void): P { const toDispose = new DisposableStore(); - const createNotification = (message: string | undefined, increment?: number): INotificationHandle | undefined => { - if (!message) { - return undefined; // we need a message at least - } + const createNotification = (message: string, increment?: number): INotificationHandle => { const primaryActions = options.primaryActions ? Array.from(options.primaryActions) : []; const secondaryActions = options.secondaryActions ? Array.from(options.secondaryActions) : []; @@ -222,21 +219,34 @@ export class ProgressService extends Disposable implements IProgressService { }; let handle: INotificationHandle | undefined; + let handleSoon: any | undefined; + + let titleAndMessage: string | undefined; // hoisted to make sure a delayed notification shows the most recent message + const updateNotification = (message?: string, increment?: number): void => { - if (!handle) { - handle = createNotification(message, increment); + + // full message (inital or update) + if (message && options.title) { + titleAndMessage = `${options.title}: ${message}`; // always prefix with overall title if we have it (https://github.com/Microsoft/vscode/issues/50932) } else { - if (typeof message === 'string') { - let newMessage: string; - if (typeof options.title === 'string') { - newMessage = `${options.title}: ${message}`; // always prefix with overall title if we have it (https://github.com/Microsoft/vscode/issues/50932) - } else { - newMessage = message; + titleAndMessage = options.title || message; + } + + if (!handle && titleAndMessage) { + // create notification now or after a delay + if (typeof options.delay === 'number' && options.delay > 0) { + if (typeof handleSoon !== 'number') { + handleSoon = setTimeout(() => handle = createNotification(titleAndMessage!, increment), options.delay); } - - handle.updateMessage(newMessage); + } else { + handle = createNotification(titleAndMessage, increment); } + } + if (handle) { + if (titleAndMessage) { + handle.updateMessage(titleAndMessage); + } if (typeof increment === 'number') { updateProgress(handle, increment); } @@ -244,7 +254,7 @@ export class ProgressService extends Disposable implements IProgressService { }; // Show initially - updateNotification(options.title); + updateNotification(); // Update based on progress const promise = callback({ @@ -255,6 +265,7 @@ export class ProgressService extends Disposable implements IProgressService { // Show progress for at least 800ms and then hide once done or canceled Promise.all([timeout(800), promise]).finally(() => { + clearTimeout(handleSoon); if (handle) { handle.close(); } diff --git a/src/vs/workbench/services/remote/common/remoteExplorerService.ts b/src/vs/workbench/services/remote/common/remoteExplorerService.ts index a0c1e0f508..41072c3ff1 100644 --- a/src/vs/workbench/services/remote/common/remoteExplorerService.ts +++ b/src/vs/workbench/services/remote/common/remoteExplorerService.ts @@ -11,6 +11,7 @@ import { ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; import { Disposable } from 'vs/base/common/lifecycle'; import { IEditableData } from 'vs/workbench/common/views'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TunnelInformation, TunnelDescription } from 'vs/platform/remote/common/remoteAuthorityResolver'; export const IRemoteExplorerService = createDecorator('remoteExplorerService'); export const REMOTE_EXPLORER_TYPE_KEY: string = 'remote.explorerType'; @@ -44,16 +45,21 @@ export interface Tunnel { closeable?: boolean; } -export function MakeAddress(host: string, port: number): string { - if (host = '127.0.0.1') { +function ToLocalHost(host: string): string { + if (host === '127.0.0.1') { host = 'localhost'; } - return host + ':' + port; + return host; +} + +export function MakeAddress(host: string, port: number): string { + return ToLocalHost(host) + ':' + port; } export class TunnelModel extends Disposable { readonly forwarded: Map; readonly detected: Map; + private _candidatesEnabled: boolean = true; private _onForwardPort: Emitter = new Emitter(); public onForwardPort: Event = this._onForwardPort.event; private _onClosePort: Emitter<{ host: string, port: number }> = new Emitter(); @@ -170,7 +176,7 @@ export class TunnelModel extends Disposable { return (this.forwarded.get(key) || this.detected.get(key))?.localAddress; } - addEnvironmentTunnels(tunnels: { remoteAddress: { port: number, host: string }, localAddress: string }[]): void { + addEnvironmentTunnels(tunnels: TunnelDescription[]): void { tunnels.forEach(tunnel => { this.detected.set(MakeAddress(tunnel.remoteAddress.host, tunnel.remoteAddress.port), { remoteHost: tunnel.remoteAddress.host, @@ -181,6 +187,10 @@ export class TunnelModel extends Disposable { }); } + set candidateEnabled(enabled: boolean) { + this._candidatesEnabled = enabled; + } + registerCandidateFinder(finder: () => Promise<{ host: string, port: number, detail: string }[]>): void { this._candidateFinder = finder; } @@ -190,8 +200,18 @@ export class TunnelModel extends Disposable { } private async updateCandidates(): Promise { + if (!this._candidatesEnabled) { + this._candidates = []; + return; + } if (this._candidateFinder) { - this._candidates = await this._candidateFinder(); + this._candidates = (await this._candidateFinder()).map(value => { + return { + host: ToLocalHost(value.host), + port: value.port, + detail: value.detail + }; + }); } } @@ -203,24 +223,24 @@ export class TunnelModel extends Disposable { export interface IRemoteExplorerService { _serviceBrand: undefined; - onDidChangeTargetType: Event; - targetType: string; + onDidChangeTargetType: Event; + targetType: string[]; readonly tunnelModel: TunnelModel; onDidChangeEditable: Event; setEditable(tunnelItem: ITunnelItem | undefined, data: IEditableData | null): void; getEditableData(tunnelItem: ITunnelItem | undefined): IEditableData | undefined; forward(remote: { host: string, port: number }, localPort?: number, name?: string): Promise; close(remote: { host: string, port: number }): Promise; - addEnvironmentTunnels(tunnels: { remoteAddress: { port: number, host: string }, localAddress: string }[] | undefined): void; + setTunnelInformation(tunnelInformation: TunnelInformation | undefined): void; registerCandidateFinder(finder: () => Promise<{ host: string, port: number, detail: string }[]>): void; refresh(): Promise; } class RemoteExplorerService implements IRemoteExplorerService { public _serviceBrand: undefined; - private _targetType: string = ''; - private readonly _onDidChangeTargetType: Emitter = new Emitter(); - public readonly onDidChangeTargetType: Event = this._onDidChangeTargetType.event; + private _targetType: string[] = []; + private readonly _onDidChangeTargetType: Emitter = new Emitter(); + public readonly onDidChangeTargetType: Event = this._onDidChangeTargetType.event; private _tunnelModel: TunnelModel; private _editable: { tunnelItem: ITunnelItem | undefined, data: IEditableData } | undefined; private readonly _onDidChangeEditable: Emitter = new Emitter(); @@ -234,15 +254,18 @@ class RemoteExplorerService implements IRemoteExplorerService { this._tunnelModel = new TunnelModel(tunnelService, storageService, configurationService); } - set targetType(name: string) { - if (this._targetType !== name) { + set targetType(name: string[]) { + // Can just compare the first element of the array since there are no target overlaps + const current: string = this._targetType.length > 0 ? this._targetType[0] : ''; + const newName: string = name.length > 0 ? name[0] : ''; + if (current !== newName) { this._targetType = name; - this.storageService.store(REMOTE_EXPLORER_TYPE_KEY, this._targetType, StorageScope.WORKSPACE); - this.storageService.store(REMOTE_EXPLORER_TYPE_KEY, this._targetType, StorageScope.GLOBAL); + this.storageService.store(REMOTE_EXPLORER_TYPE_KEY, this._targetType.toString(), StorageScope.WORKSPACE); + this.storageService.store(REMOTE_EXPLORER_TYPE_KEY, this._targetType.toString(), StorageScope.GLOBAL); this._onDidChangeTargetType.fire(this._targetType); } } - get targetType(): string { + get targetType(): string[] { return this._targetType; } @@ -258,10 +281,12 @@ class RemoteExplorerService implements IRemoteExplorerService { return this.tunnelModel.close(remote.host, remote.port); } - addEnvironmentTunnels(tunnels: { remoteAddress: { port: number, host: string }, localAddress: string }[] | undefined): void { - if (tunnels) { - this.tunnelModel.addEnvironmentTunnels(tunnels); + setTunnelInformation(tunnelInformation: TunnelInformation | undefined): void { + if (tunnelInformation && tunnelInformation.environmentTunnels) { + this.tunnelModel.addEnvironmentTunnels(tunnelInformation.environmentTunnels); } + + this.tunnelModel.candidateEnabled = tunnelInformation ? (tunnelInformation.hideCandidatePorts !== true) : true; } setEditable(tunnelItem: ITunnelItem | undefined, data: IEditableData | null): void { @@ -274,7 +299,9 @@ class RemoteExplorerService implements IRemoteExplorerService { } getEditableData(tunnelItem: ITunnelItem | undefined): IEditableData | undefined { - return (this._editable && (!tunnelItem || (this._editable.tunnelItem?.remotePort === tunnelItem.remotePort) && (this._editable.tunnelItem.remoteHost === tunnelItem.remoteHost))) ? + return (this._editable && + ((!tunnelItem && (tunnelItem === this._editable.tunnelItem)) || + (tunnelItem && (this._editable.tunnelItem?.remotePort === tunnelItem.remotePort) && (this._editable.tunnelItem.remoteHost === tunnelItem.remoteHost)))) ? this._editable.data : undefined; } diff --git a/src/vs/workbench/services/remote/common/tunnelService.ts b/src/vs/workbench/services/remote/common/tunnelService.ts new file mode 100644 index 0000000000..d558a6f625 --- /dev/null +++ b/src/vs/workbench/services/remote/common/tunnelService.ts @@ -0,0 +1,151 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ITunnelService, RemoteTunnel, ITunnelProvider } from 'vs/platform/remote/common/tunnel'; +import { Event, Emitter } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { ILogService } from 'vs/platform/log/common/log'; + +export abstract class AbstractTunnelService implements ITunnelService { + _serviceBrand: undefined; + + private _onTunnelOpened: Emitter = new Emitter(); + public onTunnelOpened: Event = this._onTunnelOpened.event; + private _onTunnelClosed: Emitter<{ host: string, port: number }> = new Emitter(); + public onTunnelClosed: Event<{ host: string, port: number }> = this._onTunnelClosed.event; + protected readonly _tunnels = new Map }>>(); + protected _tunnelProvider: ITunnelProvider | undefined; + + public constructor( + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @ILogService protected readonly logService: ILogService + ) { } + + setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable { + if (!provider) { + return { + dispose: () => { } + }; + } + this._tunnelProvider = provider; + return { + dispose: () => { + this._tunnelProvider = undefined; + } + }; + } + + public get tunnels(): Promise { + const promises: Promise[] = []; + Array.from(this._tunnels.values()).forEach(portMap => Array.from(portMap.values()).forEach(x => promises.push(x.value))); + return Promise.all(promises); + } + + dispose(): void { + for (const portMap of this._tunnels.values()) { + for (const { value } of portMap.values()) { + value.then(tunnel => tunnel.dispose()); + } + portMap.clear(); + } + this._tunnels.clear(); + } + + openTunnel(remoteHost: string | undefined, remotePort: number, localPort: number): Promise | undefined { + const remoteAuthority = this.environmentService.configuration.remoteAuthority; + if (!remoteAuthority) { + return undefined; + } + + if (!remoteHost || (remoteHost === '127.0.0.1')) { + remoteHost = 'localhost'; + } + + const resolvedTunnel = this.retainOrCreateTunnel(remoteAuthority, remoteHost, remotePort, localPort); + if (!resolvedTunnel) { + return resolvedTunnel; + } + + return resolvedTunnel.then(tunnel => { + const newTunnel = this.makeTunnel(tunnel); + if (tunnel.tunnelRemoteHost !== remoteHost || tunnel.tunnelRemotePort !== remotePort) { + this.logService.warn('Created tunnel does not match requirements of requested tunnel. Host or port mismatch.'); + } + this._onTunnelOpened.fire(newTunnel); + return newTunnel; + }); + } + + private makeTunnel(tunnel: RemoteTunnel): RemoteTunnel { + return { + tunnelRemotePort: tunnel.tunnelRemotePort, + tunnelRemoteHost: tunnel.tunnelRemoteHost, + tunnelLocalPort: tunnel.tunnelLocalPort, + localAddress: tunnel.localAddress, + dispose: () => { + const existingHost = this._tunnels.get(tunnel.tunnelRemoteHost); + if (existingHost) { + const existing = existingHost.get(tunnel.tunnelRemotePort); + if (existing) { + existing.refcount--; + this.tryDisposeTunnel(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort, existing); + } + } + } + }; + } + + private async tryDisposeTunnel(remoteHost: string, remotePort: number, tunnel: { refcount: number, readonly value: Promise }): Promise { + if (tunnel.refcount <= 0) { + const disposePromise: Promise = tunnel.value.then(tunnel => { + tunnel.dispose(); + this._onTunnelClosed.fire({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort }); + }); + if (this._tunnels.has(remoteHost)) { + this._tunnels.get(remoteHost)!.delete(remotePort); + } + return disposePromise; + } + } + + async closeTunnel(remoteHost: string, remotePort: number): Promise { + const portMap = this._tunnels.get(remoteHost); + if (portMap && portMap.has(remotePort)) { + const value = portMap.get(remotePort)!; + value.refcount = 0; + await this.tryDisposeTunnel(remoteHost, remotePort, value); + } + } + + protected addTunnelToMap(remoteHost: string, remotePort: number, tunnel: Promise) { + if (!this._tunnels.has(remoteHost)) { + this._tunnels.set(remoteHost, new Map()); + } + this._tunnels.get(remoteHost)!.set(remotePort, { refcount: 1, value: tunnel }); + } + + protected abstract retainOrCreateTunnel(remoteAuthority: string, remoteHost: string, remotePort: number, localPort?: number): Promise | undefined; +} + +export class TunnelService extends AbstractTunnelService { + protected retainOrCreateTunnel(remoteAuthority: string, remoteHost: string, remotePort: number, localPort?: number | undefined): Promise | undefined { + const portMap = this._tunnels.get(remoteHost); + const existing = portMap ? portMap.get(remotePort) : undefined; + if (existing) { + ++existing.refcount; + return existing.value; + } + + if (this._tunnelProvider) { + const tunnel = this._tunnelProvider.forwardPort({ remoteAddress: { host: remoteHost, port: remotePort } }); + if (tunnel) { + this.addTunnelToMap(remoteHost, remotePort, tunnel); + } + return tunnel; + } + return undefined; + } +} diff --git a/src/vs/workbench/services/remote/node/tunnelService.ts b/src/vs/workbench/services/remote/node/tunnelService.ts index 12988d07f4..1d0ccab6a5 100644 --- a/src/vs/workbench/services/remote/node/tunnelService.ts +++ b/src/vs/workbench/services/remote/node/tunnelService.ts @@ -5,22 +5,22 @@ import * as net from 'net'; import { Barrier } from 'vs/base/common/async'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import product from 'vs/platform/product/common/product'; import { connectRemoteAgentTunnel, IConnectionOptions } from 'vs/platform/remote/common/remoteAgentConnection'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; -import { ITunnelService, RemoteTunnel, ITunnelProvider } from 'vs/platform/remote/common/tunnel'; +import { ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; import { nodeSocketFactory } from 'vs/platform/remote/node/nodeSocketFactory'; import { ISignService } from 'vs/platform/sign/common/sign'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; import { findFreePort } from 'vs/base/node/ports'; -import { Event, Emitter } from 'vs/base/common/event'; +import { AbstractTunnelService } from 'vs/workbench/services/remote/common/tunnelService'; -export async function createRemoteTunnel(options: IConnectionOptions, tunnelRemotePort: number, tunnelLocalPort?: number): Promise { - const tunnel = new NodeRemoteTunnel(options, tunnelRemotePort, tunnelLocalPort); +export async function createRemoteTunnel(options: IConnectionOptions, tunnelRemoteHost: string, tunnelRemotePort: number, tunnelLocalPort?: number): Promise { + const tunnel = new NodeRemoteTunnel(options, tunnelRemoteHost, tunnelRemotePort, tunnelLocalPort); return tunnel.waitForReady(); } @@ -28,7 +28,7 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel { public readonly tunnelRemotePort: number; public tunnelLocalPort!: number; - public tunnelRemoteHost: string = 'localhost'; + public tunnelRemoteHost: string; public localAddress!: string; private readonly _options: IConnectionOptions; @@ -38,7 +38,7 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel { private readonly _listeningListener: () => void; private readonly _connectionListener: (socket: net.Socket) => void; - constructor(options: IConnectionOptions, tunnelRemotePort: number, private readonly suggestedLocalPort?: number) { + constructor(options: IConnectionOptions, tunnelRemoteHost: string, tunnelRemotePort: number, private readonly suggestedLocalPort?: number) { super(); this._options = options; this._server = net.createServer(); @@ -51,7 +51,7 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel { this._server.on('connection', this._connectionListener); this.tunnelRemotePort = tunnelRemotePort; - + this.tunnelRemoteHost = tunnelRemoteHost; } public dispose(): void { @@ -98,124 +98,17 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel { } } -export class TunnelService implements ITunnelService { - _serviceBrand: undefined; - - private _onTunnelOpened: Emitter = new Emitter(); - public onTunnelOpened: Event = this._onTunnelOpened.event; - private _onTunnelClosed: Emitter<{ host: string, port: number }> = new Emitter(); - public onTunnelClosed: Event<{ host: string, port: number }> = this._onTunnelClosed.event; - private readonly _tunnels = new Map }>>(); - private _tunnelProvider: ITunnelProvider | undefined; - +export class TunnelService extends AbstractTunnelService { public constructor( - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @ILogService logService: ILogService, @IRemoteAuthorityResolverService private readonly remoteAuthorityResolverService: IRemoteAuthorityResolverService, @ISignService private readonly signService: ISignService, - @ILogService private readonly logService: ILogService, - ) { } - - setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable { - if (!provider) { - return { - dispose: () => { } - }; - } - this._tunnelProvider = provider; - return { - dispose: () => { - this._tunnelProvider = undefined; - } - }; + ) { + super(environmentService, logService); } - public get tunnels(): Promise { - const promises: Promise[] = []; - Array.from(this._tunnels.values()).forEach(portMap => Array.from(portMap.values()).forEach(x => promises.push(x.value))); - return Promise.all(promises); - } - - dispose(): void { - for (const portMap of this._tunnels.values()) { - for (const { value } of portMap.values()) { - value.then(tunnel => tunnel.dispose()); - } - portMap.clear(); - } - this._tunnels.clear(); - } - - openTunnel(remoteHost: string | undefined, remotePort: number, localPort: number): Promise | undefined { - const remoteAuthority = this.environmentService.configuration.remoteAuthority; - if (!remoteAuthority) { - return undefined; - } - - if (!remoteHost || (remoteHost === '127.0.0.1')) { - remoteHost = 'localhost'; - } - - const resolvedTunnel = this.retainOrCreateTunnel(remoteAuthority, remoteHost, remotePort, localPort); - if (!resolvedTunnel) { - return resolvedTunnel; - } - - return resolvedTunnel.then(tunnel => { - const newTunnel = this.makeTunnel(tunnel); - this._onTunnelOpened.fire(newTunnel); - return newTunnel; - }); - } - - private makeTunnel(tunnel: RemoteTunnel): RemoteTunnel { - return { - tunnelRemotePort: tunnel.tunnelRemotePort, - tunnelRemoteHost: tunnel.tunnelRemoteHost, - tunnelLocalPort: tunnel.tunnelLocalPort, - localAddress: tunnel.localAddress, - dispose: () => { - const existingHost = this._tunnels.get(tunnel.tunnelRemoteHost); - if (existingHost) { - const existing = existingHost.get(tunnel.tunnelRemotePort); - if (existing) { - existing.refcount--; - this.tryDisposeTunnel(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort, existing); - } - } - } - }; - } - - private async tryDisposeTunnel(remoteHost: string, remotePort: number, tunnel: { refcount: number, readonly value: Promise }): Promise { - if (tunnel.refcount <= 0) { - const disposePromise: Promise = tunnel.value.then(tunnel => { - tunnel.dispose(); - this._onTunnelClosed.fire({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort }); - }); - if (this._tunnels.has(remoteHost)) { - this._tunnels.get(remoteHost)!.delete(remotePort); - } - return disposePromise; - } - } - - async closeTunnel(remoteHost: string, remotePort: number): Promise { - const portMap = this._tunnels.get(remoteHost); - if (portMap && portMap.has(remotePort)) { - const value = portMap.get(remotePort)!; - value.refcount = 0; - await this.tryDisposeTunnel(remoteHost, remotePort, value); - } - } - - private addTunnelToMap(remoteHost: string, remotePort: number, tunnel: Promise) { - if (!this._tunnels.has(remoteHost)) { - this._tunnels.set(remoteHost, new Map()); - } - this._tunnels.get(remoteHost)!.set(remotePort, { refcount: 1, value: tunnel }); - } - - private retainOrCreateTunnel(remoteAuthority: string, remoteHost: string, remotePort: number, localPort?: number): Promise | undefined { + protected retainOrCreateTunnel(remoteAuthority: string, remoteHost: string, remotePort: number, localPort?: number): Promise | undefined { const portMap = this._tunnels.get(remoteHost); const existing = portMap ? portMap.get(remotePort) : undefined; if (existing) { @@ -229,7 +122,7 @@ export class TunnelService implements ITunnelService { this.addTunnelToMap(remoteHost, remotePort, tunnel); } return tunnel; - } else if (remoteHost === 'localhost') { + } else { const options: IConnectionOptions = { commit: product.commit, socketFactory: nodeSocketFactory, @@ -243,11 +136,10 @@ export class TunnelService implements ITunnelService { logService: this.logService }; - const tunnel = createRemoteTunnel(options, remotePort, localPort); + const tunnel = createRemoteTunnel(options, remoteHost, remotePort, localPort); this.addTunnelToMap(remoteHost, remotePort, tunnel); return tunnel; } - return undefined; } } diff --git a/src/vs/workbench/services/search/common/search.ts b/src/vs/workbench/services/search/common/search.ts index 35b75a65db..bdd6b4c8d9 100644 --- a/src/vs/workbench/services/search/common/search.ts +++ b/src/vs/workbench/services/search/common/search.ts @@ -18,7 +18,7 @@ import { Event } from 'vs/base/common/event'; import { relative } from 'vs/base/common/path'; export const VIEWLET_ID = 'workbench.view.search'; -export const PANEL_ID = 'workbench.view.search'; +export const PANEL_ID = 'workbench.panel.search'; export const VIEW_ID = 'workbench.view.search'; export const ISearchService = createDecorator('searchService'); @@ -335,6 +335,7 @@ export interface ISearchConfigurationProperties { searchOnType: boolean; searchOnTypeDebouncePeriod: number; enableSearchEditorPreview: boolean; + searchEditorPreview: { doubleClickBehaviour: 'selectWord' | 'goToLocation' | 'openLocationToSide' }; searchEditorPreviewForceAbsolutePaths: boolean; sortOrder: SearchSortOrder; } diff --git a/src/vs/workbench/services/search/common/searchService.ts b/src/vs/workbench/services/search/common/searchService.ts index a2f8b4a777..07b126088f 100644 --- a/src/vs/workbench/services/search/common/searchService.ts +++ b/src/vs/workbench/services/search/common/searchService.ts @@ -19,7 +19,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { deserializeSearchError, FileMatch, ICachedSearchStats, IFileMatch, IFileQuery, IFileSearchStats, IFolderQuery, IProgressMessage, ISearchComplete, ISearchEngineStats, ISearchProgressItem, ISearchQuery, ISearchResultProvider, ISearchService, ITextQuery, pathIncludedInQuery, QueryType, SearchError, SearchErrorCode, SearchProviderType, isFileMatch, isProgressMessage } from 'vs/workbench/services/search/common/search'; import { addContextToEditorMatches, editorMatchesToTextSearchResults } from 'vs/workbench/services/search/common/searchHelpers'; -import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; export class SearchService extends Disposable implements ISearchService { @@ -32,7 +32,7 @@ export class SearchService extends Disposable implements ISearchService { constructor( private readonly modelService: IModelService, - private readonly untitledTextEditorService: IUntitledTextEditorService, + private readonly textFileService: ITextFileService, private readonly editorService: IEditorService, private readonly telemetryService: ITelemetryService, private readonly logService: ILogService, @@ -403,7 +403,7 @@ export class SearchService extends Disposable implements ISearchService { // Support untitled files if (resource.scheme === Schemas.untitled) { - if (!this.untitledTextEditorService.exists(resource)) { + if (!this.textFileService.untitled.exists(resource)) { return; } } @@ -457,14 +457,14 @@ export class SearchService extends Disposable implements ISearchService { export class RemoteSearchService extends SearchService { constructor( @IModelService modelService: IModelService, - @IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorService, + @ITextFileService textFileService: ITextFileService, @IEditorService editorService: IEditorService, @ITelemetryService telemetryService: ITelemetryService, @ILogService logService: ILogService, @IExtensionService extensionService: IExtensionService, @IFileService fileService: IFileService ) { - super(modelService, untitledTextEditorService, editorService, telemetryService, logService, extensionService, fileService); + super(modelService, textFileService, editorService, telemetryService, logService, extensionService, fileService); } } diff --git a/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts b/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts index 9b9b62ce6c..5ac5951cab 100644 --- a/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts +++ b/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts @@ -532,7 +532,7 @@ export type IRgBytesOrText = { bytes: string } | { text: string }; export function fixRegexNewline(pattern: string): string { // Replace an unescaped $ at the end of the pattern with \r?$ - // Match $ preceeded by none or even number of literal \ + // Match $ preceded by none or even number of literal \ return pattern.replace(/(?<=[^\\]|^)(\\\\)*\\n/g, '$1\\r?\\n'); } diff --git a/src/vs/workbench/services/search/node/searchService.ts b/src/vs/workbench/services/search/node/searchService.ts index 5d43f9e2b8..db1c08d76c 100644 --- a/src/vs/workbench/services/search/node/searchService.ts +++ b/src/vs/workbench/services/search/node/searchService.ts @@ -21,7 +21,7 @@ import { SearchChannelClient } from './searchIpc'; import { SearchService } from 'vs/workbench/services/search/common/searchService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -31,7 +31,7 @@ import { parseSearchPort } from 'vs/platform/environment/node/environmentService export class LocalSearchService extends SearchService { constructor( @IModelService modelService: IModelService, - @IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorService, + @ITextFileService textFileService: ITextFileService, @IEditorService editorService: IEditorService, @ITelemetryService telemetryService: ITelemetryService, @ILogService logService: ILogService, @@ -40,7 +40,7 @@ export class LocalSearchService extends SearchService { @IWorkbenchEnvironmentService readonly environmentService: IWorkbenchEnvironmentService, @IInstantiationService readonly instantiationService: IInstantiationService ) { - super(modelService, untitledTextEditorService, editorService, telemetryService, logService, extensionService, fileService); + super(modelService, textFileService, editorService, telemetryService, logService, extensionService, fileService); this.diskSearch = instantiationService.createInstance(DiskSearch, !environmentService.isBuilt || environmentService.verbose, parseSearchPort(environmentService.args, environmentService.isBuilt)); diff --git a/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts b/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts index a48300e306..6371467a32 100644 --- a/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts +++ b/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts @@ -322,8 +322,15 @@ export abstract class AbstractTextMateService extends Disposable implements ITex } public async createGrammar(modeId: string): Promise { + const languageId = this._modeService.getLanguageIdentifier(modeId); + if (!languageId) { + return null; + } const grammarFactory = await this._getOrCreateGrammarFactory(); - const { grammar } = await grammarFactory.createGrammar(this._modeService.getLanguageIdentifier(modeId)!.id); + if (!grammarFactory.has(languageId.id)) { + return null; + } + const { grammar } = await grammarFactory.createGrammar(languageId.id); return grammar; } diff --git a/src/vs/workbench/services/textfile/browser/browserTextFileService.ts b/src/vs/workbench/services/textfile/browser/browserTextFileService.ts index b8877c162f..faa788de02 100644 --- a/src/vs/workbench/services/textfile/browser/browserTextFileService.ts +++ b/src/vs/workbench/services/textfile/browser/browserTextFileService.ts @@ -7,7 +7,6 @@ import { AbstractTextFileService } from 'vs/workbench/services/textfile/browser/ import { ITextFileService, IResourceEncodings, IResourceEncoding, ModelState } from 'vs/workbench/services/textfile/common/textfiles'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle'; -import { Schemas } from 'vs/base/common/network'; export class BrowserTextFileService extends AbstractTextFileService { @@ -17,49 +16,21 @@ export class BrowserTextFileService extends AbstractTextFileService { } }; - protected onBeforeShutdown(reason: ShutdownReason): boolean { - // Web: we cannot perform long running in the shutdown phase - // As such we need to check sync if there are any dirty files - // that have not been backed up yet and then prevent the shutdown - // if that is the case. - return this.doBeforeShutdownSync(); + protected registerListeners(): void { + super.registerListeners(); + + // Lifecycle + this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdown(event.reason))); } - private doBeforeShutdownSync(): boolean { - if (this.models.getAll().some(model => model.hasState(ModelState.PENDING_SAVE) || model.hasState(ModelState.PENDING_AUTO_SAVE))) { + protected onBeforeShutdown(reason: ShutdownReason): boolean { + if (this.files.getAll().some(model => model.hasState(ModelState.PENDING_SAVE))) { + console.warn('Unload prevented: pending file saves'); + return true; // files are pending to be saved: veto } - const dirtyResources = this.getDirty(); - if (!dirtyResources.length) { - return false; // no dirty: no veto - } - - if (!this.filesConfigurationService.isHotExitEnabled) { - return true; // dirty without backup: veto - } - - for (const dirtyResource of dirtyResources) { - let hasBackup = false; - - if (this.fileService.canHandleResource(dirtyResource)) { - const model = this.models.get(dirtyResource); - hasBackup = !!(model?.hasBackup()); - } else if (dirtyResource.scheme === Schemas.untitled) { - hasBackup = this.untitledTextEditorService.hasBackup(dirtyResource); - } - - if (!hasBackup) { - console.warn('Unload prevented: pending backups'); - return true; // dirty without backup: veto - } - } - - return false; // dirty with backups: no veto - } - - protected async getWindowCount(): Promise { - return 1; // Browser only ever is 1 window + return false; } } diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index aab09abdf3..62c6573182 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -6,16 +6,13 @@ import * as nls from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import { Emitter, AsyncEmitter } from 'vs/base/common/event'; -import * as platform from 'vs/base/common/platform'; -import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; -import { IResult, ITextFileOperationResult, ITextFileService, ITextFileStreamContent, ITextFileEditorModelManager, ITextFileEditorModel, ModelState, ITextFileContent, IResourceEncodings, IReadTextFileOptions, IWriteTextFileOptions, toBufferOrReadable, TextFileOperationError, TextFileOperationResult, FileOperationWillRunEvent, FileOperationDidRunEvent, ITextFileSaveOptions } from 'vs/workbench/services/textfile/common/textfiles'; -import { SaveReason, IRevertOptions } from 'vs/workbench/common/editor'; -import { ILifecycleService, ShutdownReason, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IFileService, FileOperationError, FileOperationResult, HotExitConfiguration, IFileStatWithMetadata, ICreateFileOptions, FileOperation } from 'vs/platform/files/common/files'; +import { IResult, ITextFileOperationResult, ITextFileService, ITextFileStreamContent, ITextFileEditorModel, ITextFileContent, IResourceEncodings, IReadTextFileOptions, IWriteTextFileOptions, toBufferOrReadable, TextFileOperationError, TextFileOperationResult, FileOperationWillRunEvent, FileOperationDidRunEvent, ITextFileSaveOptions } from 'vs/workbench/services/textfile/common/textfiles'; +import { IRevertOptions, IEncodingSupport } from 'vs/workbench/common/editor'; +import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { IFileService, FileOperationError, FileOperationResult, IFileStatWithMetadata, ICreateFileOptions, FileOperation } from 'vs/platform/files/common/files'; import { Disposable } from 'vs/base/common/lifecycle'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; +import { IUntitledTextEditorService, IUntitledTextEditorModelManager } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { UntitledTextEditorModel } from 'vs/workbench/common/editor/untitledTextEditorModel'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -24,19 +21,17 @@ import { Schemas } from 'vs/base/common/network'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { createTextBufferFactoryFromSnapshot, createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { isEqualOrParent, isEqual, joinPath, dirname, extname, basename, toLocalResource } from 'vs/base/common/resources'; -import { IDialogService, IFileDialogService, ISaveDialogOptions, IConfirmation, ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; -import { IModeService } from 'vs/editor/common/services/modeService'; +import { isEqualOrParent, isEqual, joinPath, dirname, basename, toLocalResource } from 'vs/base/common/resources'; +import { IDialogService, IFileDialogService, IConfirmation } from 'vs/platform/dialogs/common/dialogs'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { coalesce } from 'vs/base/common/arrays'; -import { trim } from 'vs/base/common/strings'; import { VSBuffer } from 'vs/base/common/buffer'; -import { ITextSnapshot } from 'vs/editor/common/model'; +import { ITextSnapshot, ITextModel } from 'vs/editor/common/model'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; -import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; +import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; /** * The workbench file service implementation implements the raw file service spec and adds additional methods on top. @@ -55,238 +50,42 @@ export abstract class AbstractTextFileService extends Disposable implements ITex //#endregion - private _models: TextFileEditorModelManager; - get models(): ITextFileEditorModelManager { return this._models; } + readonly files = this._register(this.instantiationService.createInstance(TextFileEditorModelManager)); + + private _untitled: IUntitledTextEditorModelManager; + get untitled(): IUntitledTextEditorModelManager { return this._untitled; } abstract get encoding(): IResourceEncodings; constructor( - @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IFileService protected readonly fileService: IFileService, - @IUntitledTextEditorService protected readonly untitledTextEditorService: IUntitledTextEditorService, - @ILifecycleService private readonly lifecycleService: ILifecycleService, + @IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorService, + @ILifecycleService protected readonly lifecycleService: ILifecycleService, @IInstantiationService protected readonly instantiationService: IInstantiationService, - @IModeService private readonly modeService: IModeService, @IModelService private readonly modelService: IModelService, @IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService, - @INotificationService private readonly notificationService: INotificationService, - @IBackupFileService private readonly backupFileService: IBackupFileService, @IHistoryService private readonly historyService: IHistoryService, @IDialogService private readonly dialogService: IDialogService, @IFileDialogService private readonly fileDialogService: IFileDialogService, @IEditorService private readonly editorService: IEditorService, @ITextResourceConfigurationService protected readonly textResourceConfigurationService: ITextResourceConfigurationService, - @IFilesConfigurationService protected readonly filesConfigurationService: IFilesConfigurationService + @IFilesConfigurationService protected readonly filesConfigurationService: IFilesConfigurationService, + @ITextModelService private readonly textModelService: ITextModelService ) { super(); - this._models = this._register(instantiationService.createInstance(TextFileEditorModelManager)); + this._untitled = untitledTextEditorService; this.registerListeners(); } - //#region event handling - - private registerListeners(): void { + protected registerListeners(): void { // Lifecycle - this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdown(event.reason))); this.lifecycleService.onShutdown(this.dispose, this); - - // Auto save changes - this._register(this.filesConfigurationService.onAutoSaveConfigurationChange(() => this.onAutoSaveConfigurationChange())); } - private onAutoSaveConfigurationChange(): void { - - // save all dirty when enabling auto save - if (this.filesConfigurationService.getAutoSaveMode() !== AutoSaveMode.OFF) { - this.saveAll(); - } - } - - protected onBeforeShutdown(reason: ShutdownReason): boolean | Promise { - - // Dirty files need treatment on shutdown - const dirty = this.getDirty(); - if (dirty.length) { - - // If auto save is enabled, save all files and then check again for dirty files - // We DO NOT run any save participant if we are in the shutdown phase for performance reasons - if (this.filesConfigurationService.getAutoSaveMode() !== AutoSaveMode.OFF) { - return this.saveAll(false /* files only */, { skipSaveParticipants: true }).then(() => { - - // If we still have dirty files, we either have untitled ones or files that cannot be saved - const remainingDirty = this.getDirty(); - if (remainingDirty.length) { - return this.handleDirtyBeforeShutdown(remainingDirty, reason); - } - - return false; - }); - } - - // Auto save is not enabled - return this.handleDirtyBeforeShutdown(dirty, reason); - } - - // No dirty files: no veto - return this.noVeto({ cleanUpBackups: true }); - } - - private handleDirtyBeforeShutdown(dirty: URI[], reason: ShutdownReason): boolean | Promise { - - // If hot exit is enabled, backup dirty files and allow to exit without confirmation - if (this.filesConfigurationService.isHotExitEnabled) { - return this.backupBeforeShutdown(dirty, reason).then(didBackup => { - if (didBackup) { - return this.noVeto({ cleanUpBackups: false }); // no veto and no backup cleanup (since backup was successful) - } - - // since a backup did not happen, we have to confirm for the dirty files now - return this.confirmBeforeShutdown(); - }, error => { - this.notificationService.error(nls.localize('files.backup.failSave', "Files that are dirty could not be written to the backup location (Error: {0}). Try saving your files first and then exit.", error.message)); - - return true; // veto, the backups failed - }); - } - - // Otherwise just confirm from the user what to do with the dirty files - return this.confirmBeforeShutdown(); - } - - private async backupBeforeShutdown(dirtyToBackup: URI[], reason: ShutdownReason): Promise { - // When quit is requested skip the confirm callback and attempt to backup all workspaces. - // When quit is not requested the confirm callback should be shown when the window being - // closed is the only VS Code window open, except for on Mac where hot exit is only - // ever activated when quit is requested. - - let doBackup: boolean | undefined; - switch (reason) { - case ShutdownReason.CLOSE: - if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.filesConfigurationService.hotExitConfiguration === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) { - doBackup = true; // backup if a folder is open and onExitAndWindowClose is configured - } else if (await this.getWindowCount() > 1 || platform.isMacintosh) { - doBackup = false; // do not backup if a window is closed that does not cause quitting of the application - } else { - doBackup = true; // backup if last window is closed on win/linux where the application quits right after - } - break; - - case ShutdownReason.QUIT: - doBackup = true; // backup because next start we restore all backups - break; - - case ShutdownReason.RELOAD: - doBackup = true; // backup because after window reload, backups restore - break; - - case ShutdownReason.LOAD: - if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.filesConfigurationService.hotExitConfiguration === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) { - doBackup = true; // backup if a folder is open and onExitAndWindowClose is configured - } else { - doBackup = false; // do not backup because we are switching contexts - } - break; - } - - if (!doBackup) { - return false; - } - - await this.backupAll(dirtyToBackup); - - return true; - } - - protected abstract getWindowCount(): Promise; - - private backupAll(dirtyToBackup: URI[]): Promise { - - // split up between files and untitled - const filesToBackup: ITextFileEditorModel[] = []; - const untitledToBackup: URI[] = []; - dirtyToBackup.forEach(dirty => { - if (this.fileService.canHandleResource(dirty)) { - const model = this.models.get(dirty); - if (model) { - filesToBackup.push(model); - } - } else if (dirty.scheme === Schemas.untitled) { - untitledToBackup.push(dirty); - } - }); - - return this.doBackupAll(filesToBackup, untitledToBackup); - } - - private async doBackupAll(dirtyFileModels: ITextFileEditorModel[], untitledResources: URI[]): Promise { - - // Handle file resources first - await Promise.all(dirtyFileModels.map(model => model.backup())); - - // Handle untitled resources - await Promise.all(untitledResources - .filter(untitled => this.untitledTextEditorService.exists(untitled)) - .map(async untitled => (await this.untitledTextEditorService.loadOrCreate({ resource: untitled })).backup())); - } - - private async confirmBeforeShutdown(): Promise { - const confirm = await this.fileDialogService.showSaveConfirm(this.getDirty()); - - // Save - if (confirm === ConfirmResult.SAVE) { - const result = await this.saveAll(true /* includeUntitled */, { skipSaveParticipants: true }); - - if (result.results.some(r => r.error)) { - return true; // veto if some saves failed - } - - return this.noVeto({ cleanUpBackups: true }); - } - - // Don't Save - else if (confirm === ConfirmResult.DONT_SAVE) { - - // Make sure to revert untitled so that they do not restore - // see https://github.com/Microsoft/vscode/issues/29572 - this.untitledTextEditorService.revertAll(); - - return this.noVeto({ cleanUpBackups: true }); - } - - // Cancel - else if (confirm === ConfirmResult.CANCEL) { - return true; // veto - } - - return false; - } - - private noVeto(options: { cleanUpBackups: boolean }): boolean | Promise { - if (!options.cleanUpBackups) { - return false; - } - - if (this.lifecycleService.phase < LifecyclePhase.Restored) { - return false; // if editors have not restored, we are not up to speed with backups and thus should not clean them - } - - return this.cleanupBackupsBeforeShutdown().then(() => false, () => false); - } - - protected async cleanupBackupsBeforeShutdown(): Promise { - if (this.environmentService.isExtensionDevelopment) { - return; - } - - await this.backupFileService.discardAllWorkspaceBackups(); - } - - //#endregion - - //#region primitives (read, create, move, delete, update) + //#region text file IO primitives (read, create, move, delete, update) async read(resource: URI, options?: IReadTextFileOptions): Promise { const content = await this.fileService.readFile(resource, options); @@ -353,7 +152,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex // it again to make sure it is up to date with the contents // we just wrote into the underlying resource by calling // revert() - const existingModel = this.models.get(resource); + const existingModel = this.files.get(resource); if (existingModel && !existingModel.isDisposed()) { await existingModel.revert(); } @@ -377,8 +176,8 @@ export abstract class AbstractTextFileService extends Disposable implements ITex // before event await this._onWillRunOperation.fireAsync({ operation: FileOperation.DELETE, target: resource }, CancellationToken.None); - const dirtyFiles = this.getDirty().filter(dirty => isEqualOrParent(dirty, resource)); - await this.revertAll(dirtyFiles, { soft: true }); + const dirtyFiles = this.getDirtyFileModels().map(dirtyFileModel => dirtyFileModel.resource).filter(dirty => isEqualOrParent(dirty, resource)); + await this.doRevertFiles(dirtyFiles, { soft: true }); await this.fileService.del(resource, options); @@ -444,7 +243,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex // in order to move and copy, we need to soft revert all dirty models, // both from the source as well as the target if any const dirtyModels = [...sourceModels, ...conflictingModels].filter(model => model.isDirty()); - await this.revertAll(dirtyModels.map(dirtyModel => dirtyModel.resource), { soft: true }); + await this.doRevertFiles(dirtyModels.map(dirtyModel => dirtyModel.resource), { soft: true }); // now we can rename the source to target via file operation let stat: IFileStatWithMetadata; @@ -469,7 +268,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex // we know the file has changed on disk after the move and the // model might have still existed with the previous state. this // ensures we are not tracking a stale state. - const restoredModel = await this.models.loadOrCreate(modelToRestore.resource, { reload: { async: false }, encoding: modelToRestore.encoding, mode: modelToRestore.mode }); + const restoredModel = await this.files.resolve(modelToRestore.resource, { reload: { async: false }, encoding: modelToRestore.encoding, mode: modelToRestore.mode }); // restore previous dirty content if any and ensure to mark // the model as dirty @@ -488,271 +287,157 @@ export abstract class AbstractTextFileService extends Disposable implements ITex //#endregion - //#region save/revert + //#region save async save(resource: URI, options?: ITextFileSaveOptions): Promise { - // Run a forced save if we detect the file is not dirty so that save participants can still run - if (options?.force && this.fileService.canHandleResource(resource) && !this.isDirty(resource)) { - const model = this._models.get(resource); + // Untitled + if (resource.scheme === Schemas.untitled) { + const model = this.untitled.get(resource); if (model) { - options.reason = SaveReason.EXPLICIT; + let targetUri: URI | undefined; + // Untitled with associated file path don't need to prompt + if (model.hasAssociatedFilePath) { + targetUri = toLocalResource(resource, this.environmentService.configuration.remoteAuthority); + } + + // Otherwise ask user + else { + targetUri = await this.promptForPath(resource, this.suggestFilePath(resource)); + } + + // Save as if target provided + if (targetUri) { + await this.saveAs(resource, targetUri, options); + + return true; + } + } + } + + // File + else { + const model = this.files.get(resource); + if (model) { + + // Save with options await model.save(options); return !model.isDirty(); } } - return !(await this.saveAll([resource], options)).results.some(result => result.error); + return false; } - saveAll(includeUntitled?: boolean, options?: ITextFileSaveOptions): Promise; - saveAll(resources: URI[], options?: ITextFileSaveOptions): Promise; - saveAll(arg1?: boolean | URI[], options?: ITextFileSaveOptions): Promise { - - // get all dirty - let toSave: URI[] = []; - if (Array.isArray(arg1)) { - toSave = this.getDirty(arg1); - } else { - toSave = this.getDirty(); - } - - // split up between files and untitled - const filesToSave: URI[] = []; - const untitledToSave: URI[] = []; - toSave.forEach(resourceToSave => { - if ((Array.isArray(arg1) || arg1 === true /* includeUntitled */) && resourceToSave.scheme === Schemas.untitled) { - untitledToSave.push(resourceToSave); - } else { - filesToSave.push(resourceToSave); - } - }); - - return this.doSaveAll(filesToSave, untitledToSave, options); - } - - private async doSaveAll(fileResources: URI[], untitledResources: URI[], options?: ITextFileSaveOptions): Promise { - - // Handle files first that can just be saved - const result = await this.doSaveAllFiles(fileResources, options); - - // Preflight for untitled to handle cancellation from the dialog - const targetsForUntitled: URI[] = []; - for (const untitled of untitledResources) { - if (this.untitledTextEditorService.exists(untitled)) { - let targetUri: URI; - - // Untitled with associated file path don't need to prompt - if (this.untitledTextEditorService.hasAssociatedFilePath(untitled)) { - targetUri = toLocalResource(untitled, this.environmentService.configuration.remoteAuthority); - } - - // Otherwise ask user - else { - const targetPath = await this.promptForPath(untitled, this.suggestFileName(untitled)); - if (!targetPath) { - return { results: [...fileResources, ...untitledResources].map(r => ({ source: r })) }; - } - - targetUri = targetPath; - } - - targetsForUntitled.push(targetUri); - } - } - - // Handle untitled - await Promise.all(targetsForUntitled.map(async (target, index) => { - const uri = await this.saveAs(untitledResources[index], target); - - result.results.push({ - source: untitledResources[index], - target: uri, - error: !uri // the operation was canceled or failed, so mark as error - }); - })); - - return result; - } - - protected async promptForPath(resource: URI, defaultUri: URI, availableFileSystems?: readonly string[]): Promise { + protected async promptForPath(resource: URI, defaultUri: URI, availableFileSystems?: string[]): Promise { // Help user to find a name for the file by opening it first await this.editorService.openEditor({ resource, options: { revealIfOpened: true, preserveFocus: true } }); - return this.fileDialogService.pickFileToSave(this.getSaveDialogOptions(defaultUri, availableFileSystems)); + return this.fileDialogService.pickFileToSave(defaultUri, availableFileSystems); } - private getSaveDialogOptions(defaultUri: URI, availableFileSystems?: readonly string[]): ISaveDialogOptions { - const options: ISaveDialogOptions = { - defaultUri, - title: nls.localize('saveAsTitle', "Save As"), - availableFileSystems, - }; - - // Filters are only enabled on Windows where they work properly - if (!platform.isWindows) { - return options; - } - - interface IFilter { name: string; extensions: string[]; } - - // Build the file filter by using our known languages - const ext: string | undefined = defaultUri ? extname(defaultUri) : undefined; - let matchingFilter: IFilter | undefined; - const filters: IFilter[] = coalesce(this.modeService.getRegisteredLanguageNames().map(languageName => { - const extensions = this.modeService.getExtensions(languageName); - if (!extensions || !extensions.length) { - return null; - } - - const filter: IFilter = { name: languageName, extensions: extensions.slice(0, 10).map(e => trim(e, '.')) }; - - if (ext && extensions.indexOf(ext) >= 0) { - matchingFilter = filter; - - return null; // matching filter will be added last to the top - } - - return filter; - })); - - // Filters are a bit weird on Windows, based on having a match or not: - // Match: we put the matching filter first so that it shows up selected and the all files last - // No match: we put the all files filter first - const allFilesFilter = { name: nls.localize('allFiles', "All Files"), extensions: ['*'] }; - if (matchingFilter) { - filters.unshift(matchingFilter); - filters.unshift(allFilesFilter); - } else { - filters.unshift(allFilesFilter); - } - - // Allow to save file without extension - filters.push({ name: nls.localize('noExt', "No Extension"), extensions: [''] }); - - options.filters = filters; - - return options; - } - - private async doSaveAllFiles(resources?: URI[], options: ITextFileSaveOptions = Object.create(null)): Promise { - const dirtyFileModels = this.getDirtyFileModels(Array.isArray(resources) ? resources : undefined /* Save All */) - .filter(model => { - if ((model.hasState(ModelState.CONFLICT) || model.hasState(ModelState.ERROR)) && (options.reason === SaveReason.AUTO || options.reason === SaveReason.FOCUS_CHANGE || options.reason === SaveReason.WINDOW_CHANGE)) { - return false; // if model is in save conflict or error, do not save unless save reason is explicit or not provided at all - } - - return true; - }); - - const mapResourceToResult = new ResourceMap(); - dirtyFileModels.forEach(dirtyModel => { - mapResourceToResult.set(dirtyModel.resource, { - source: dirtyModel.resource - }); - }); - - await Promise.all(dirtyFileModels.map(async model => { - await model.save(options); - - // If model is still dirty, mark the resulting operation as error - if (model.isDirty()) { - const result = mapResourceToResult.get(model.resource); - if (result) { - result.error = true; - } - } - })); - - return { results: mapResourceToResult.values() }; - } - - private getFileModels(arg1?: URI | URI[]): ITextFileEditorModel[] { - if (Array.isArray(arg1)) { + private getFileModels(resources?: URI | URI[]): ITextFileEditorModel[] { + if (Array.isArray(resources)) { const models: ITextFileEditorModel[] = []; - arg1.forEach(resource => { - models.push(...this.getFileModels(resource)); - }); + resources.forEach(resource => models.push(...this.getFileModels(resource))); return models; } - return this._models.getAll(arg1); + return this.files.getAll(resources); } - private getDirtyFileModels(resources?: URI | URI[]): ITextFileEditorModel[] { + private getDirtyFileModels(resources?: URI[]): ITextFileEditorModel[] { return this.getFileModels(resources).filter(model => model.isDirty()); } - async saveAs(resource: URI, targetResource?: URI, options?: ITextFileSaveOptions): Promise { + async saveAs(source: URI, target?: URI, options?: ITextFileSaveOptions): Promise { // Get to target resource - if (!targetResource) { - let dialogPath = resource; - if (resource.scheme === Schemas.untitled) { - dialogPath = this.suggestFileName(resource); + if (!target) { + let dialogPath = source; + if (source.scheme === Schemas.untitled) { + dialogPath = this.suggestFilePath(source); } - targetResource = await this.promptForPath(resource, dialogPath, options ? options.availableFileSystems : undefined); + target = await this.promptForPath(source, dialogPath, options?.availableFileSystems); } - if (!targetResource) { + if (!target) { return undefined; // user canceled // {{SQL CARBON EDIT}} strict-null-check } // Just save if target is same as models own resource - if (resource.toString() === targetResource.toString()) { - await this.save(resource, options); + if (source.toString() === target.toString()) { + await this.save(source, options); - return resource; + return source; } // Do it - return this.doSaveAs(resource, targetResource, options); + return this.doSaveAs(source, target, options); } - private async doSaveAs(resource: URI, target: URI, options?: ITextFileSaveOptions): Promise { + private async doSaveAs(source: URI, target: URI, options?: ITextFileSaveOptions): Promise { + let success = false; - // Retrieve text model from provided resource if any - let model: ITextFileEditorModel | UntitledTextEditorModel | undefined; - if (this.fileService.canHandleResource(resource)) { - model = this._models.get(resource); - } else if (resource.scheme === Schemas.untitled && this.untitledTextEditorService.exists(resource)) { - model = await this.untitledTextEditorService.loadOrCreate({ resource }); + // If the source is an existing text file model, we can directly + // use that model to copy the contents to the target destination + const textFileModel = this.files.get(source); + if (textFileModel && textFileModel.isResolved()) { + success = await this.doSaveAsTextFile(textFileModel, source, target, options); } - // We have a model: Use it (can be null e.g. if this file is binary and not a text file or was never opened before) - let result: boolean; - if (model) { - result = await this.doSaveTextFileAs(model, resource, target, options); + // Otherwise if the source can be handled by the file service + // we can simply invoke the copy() function to save as + else if (this.fileService.canHandleResource(source)) { + await this.fileService.copy(source, target); + + success = true; } - // Otherwise we can only copy + // Next, if the source does not seem to be a file, we try to + // resolve a text model from the resource to get at the + // contents and additional meta data (e.g. encoding). + else if (this.textModelService.hasTextModelContentProvider(source.scheme)) { + const modelReference = await this.textModelService.createModelReference(source); + success = await this.doSaveAsTextFile(modelReference.object, source, target, options); + + modelReference.dispose(); // free up our use of the reference + } + + // Finally we simply check if we can find a editor model that + // would give us access to the contents. else { - await this.fileService.copy(resource, target); - - result = true; + const textModel = this.modelService.getModel(source); + if (textModel) { + success = await this.doSaveAsTextFile(textModel, source, target, options); + } } - // Return early if the operation was not running - if (!result) { - return target; + // Revert the source if result is success + if (success) { + await this.revert(source); } - // Revert the source - await this.revert(resource); - return target; } - private async doSaveTextFileAs(sourceModel: ITextFileEditorModel | UntitledTextEditorModel, resource: URI, target: URI, options?: ITextFileSaveOptions): Promise { + private async doSaveAsTextFile(sourceModel: IResolvedTextEditorModel | ITextModel, source: URI, target: URI, options?: ITextFileSaveOptions): Promise { + + // Find source encoding if any + let sourceModelEncoding: string | undefined = undefined; + const sourceModelWithEncodingSupport = (sourceModel as unknown as IEncodingSupport); + if (typeof sourceModelWithEncodingSupport.getEncoding === 'function') { + sourceModelEncoding = sourceModelWithEncodingSupport.getEncoding(); + } // Prefer an existing model if it is already loaded for the given target resource let targetExists: boolean = false; - let targetModel = this.models.get(target); + let targetModel = this.files.get(target); if (targetModel?.isResolved()) { targetExists = true; } @@ -761,70 +446,83 @@ export abstract class AbstractTextFileService extends Disposable implements ITex else { targetExists = await this.fileService.exists(target); - // create target model adhoc if file does not exist yet + // create target file adhoc if it does not exist yet if (!targetExists) { await this.create(target, ''); } - // Carry over the mode if this is an untitled file and the mode was picked by the user - let mode: string | undefined; - if (sourceModel instanceof UntitledTextEditorModel) { - mode = sourceModel.getMode(); - if (mode === PLAINTEXT_MODE_ID) { - mode = undefined; // never enforce plain text mode when moving as it is unspecific - } - } + try { + targetModel = await this.files.resolve(target, { encoding: sourceModelEncoding }); + } catch (error) { + // if the target already exists and was not created by us, it is possible + // that we cannot load the target as text model if it is binary or too + // large. in that case we have to delete the target file first and then + // re-run the operation. + if (targetExists) { + if ( + (error).textFileOperationResult === TextFileOperationResult.FILE_IS_BINARY || + (error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE + ) { + await this.fileService.del(target); - targetModel = await this.models.loadOrCreate(target, { encoding: sourceModel.getEncoding(), mode }); + return this.doSaveAsTextFile(sourceModel, source, target, options); + } + } + + throw error; + } } - try { - - // Confirm to overwrite if we have an untitled file with associated file where - // the file actually exists on disk and we are instructed to save to that file - // path. This can happen if the file was created after the untitled file was opened. - // See https://github.com/Microsoft/vscode/issues/67946 - let write: boolean; - if (sourceModel instanceof UntitledTextEditorModel && sourceModel.hasAssociatedFilePath && targetExists && isEqual(target, toLocalResource(sourceModel.resource, this.environmentService.configuration.remoteAuthority))) { - write = await this.confirmOverwrite(target); - } else { - write = true; - } - - if (!write) { - return false; - } - - // take over model value, encoding and mode (only if more specific) from source model - targetModel.updatePreferredEncoding(sourceModel.getEncoding()); - if (sourceModel.isResolved() && targetModel.isResolved()) { - this.modelService.updateModel(targetModel.textEditorModel, createTextBufferFactoryFromSnapshot(sourceModel.createSnapshot())); - - const sourceMode = sourceModel.textEditorModel.getLanguageIdentifier(); - const targetMode = targetModel.textEditorModel.getLanguageIdentifier(); - if (sourceMode.language !== PLAINTEXT_MODE_ID && targetMode.language === PLAINTEXT_MODE_ID) { - targetModel.textEditorModel.setMode(sourceMode); // only use if more specific than plain/text - } - } - - // save model - await targetModel.save(options); - - return true; - } catch (error) { - - // binary model: delete the file and run the operation again - if ( - (error).textFileOperationResult === TextFileOperationResult.FILE_IS_BINARY || - (error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE - ) { - await this.fileService.del(target); - - return this.doSaveTextFileAs(sourceModel, resource, target, options); - } - - throw error; + // Confirm to overwrite if we have an untitled file with associated file where + // the file actually exists on disk and we are instructed to save to that file + // path. This can happen if the file was created after the untitled file was opened. + // See https://github.com/Microsoft/vscode/issues/67946 + let write: boolean; + if (sourceModel instanceof UntitledTextEditorModel && sourceModel.hasAssociatedFilePath && targetExists && isEqual(target, toLocalResource(sourceModel.resource, this.environmentService.configuration.remoteAuthority))) { + write = await this.confirmOverwrite(target); + } else { + write = true; } + + if (!write) { + return false; + } + + let sourceTextModel: ITextModel | undefined = undefined; + if (sourceModel instanceof BaseTextEditorModel) { + if (sourceModel.isResolved()) { + sourceTextModel = sourceModel.textEditorModel; + } + } else { + sourceTextModel = sourceModel as ITextModel; + } + + let targetTextModel: ITextModel | undefined = undefined; + if (targetModel.isResolved()) { + targetTextModel = targetModel.textEditorModel; + } + + // take over model value, encoding and mode (only if more specific) from source model + if (sourceTextModel && targetTextModel) { + + // encoding + targetModel.updatePreferredEncoding(sourceModelEncoding); + + // content + this.modelService.updateModel(targetTextModel, createTextBufferFactoryFromSnapshot(sourceTextModel.createSnapshot())); + + // mode + const sourceMode = sourceTextModel.getLanguageIdentifier(); + const targetMode = targetTextModel.getLanguageIdentifier(); + if (sourceMode.language !== PLAINTEXT_MODE_ID && targetMode.language === PLAINTEXT_MODE_ID) { + targetTextModel.setMode(sourceMode); // only use if more specific than plain/text + } + } + + // save model + await targetModel.save(options); + + return true; } private async confirmOverwrite(resource: URI): Promise { @@ -838,8 +536,8 @@ export abstract class AbstractTextFileService extends Disposable implements ITex return (await this.dialogService.confirm(confirm)).confirmed; } - private suggestFileName(untitledResource: URI): URI { - const untitledFileName = this.untitledTextEditorService.suggestFileName(untitledResource); + private suggestFilePath(untitledResource: URI): URI { + const untitledFileName = this.untitled.get(untitledResource)?.suggestFileName() ?? basename(untitledResource); const remoteAuthority = this.environmentService.configuration.remoteAuthority; const schemeFilter = remoteAuthority ? Schemas.vscodeRemote : Schemas.file; @@ -857,23 +555,27 @@ export abstract class AbstractTextFileService extends Disposable implements ITex return untitledResource.with({ path: untitledFileName }); } + //#endregion + + //#region revert + async revert(resource: URI, options?: IRevertOptions): Promise { - return !(await this.revertAll([resource], options)).results.some(result => result.error); + + // Untitled + if (resource.scheme === Schemas.untitled) { + const model = this.untitled.get(resource); + if (model) { + return model.revert(options); + } + + return false; + } + + // File + return !(await this.doRevertFiles([resource], options)).results.some(result => result.error); } - async revertAll(resources?: URI[], options?: IRevertOptions): Promise { - - // Revert files first - const revertOperationResult = await this.doRevertAllFiles(resources, options); - - // Revert untitled - const untitledReverted = this.untitledTextEditorService.revertAll(resources); - untitledReverted.forEach(untitled => revertOperationResult.results.push({ source: untitled })); - - return revertOperationResult; - } - - private async doRevertAllFiles(resources?: URI[], options?: IRevertOptions): Promise { + private async doRevertFiles(resources: URI[], options?: IRevertOptions): Promise { const fileModels = options?.force ? this.getFileModels(resources) : this.getDirtyFileModels(resources); const mapResourceToResult = new ResourceMap(); @@ -911,35 +613,25 @@ export abstract class AbstractTextFileService extends Disposable implements ITex return { results: mapResourceToResult.values() }; } - getDirty(resources?: URI[]): URI[] { + //#endregion - // Collect files - const dirty = this.getDirtyFileModels(resources).map(dirtyFileModel => dirtyFileModel.resource); + //#region dirty - // Add untitled ones - dirty.push(...this.untitledTextEditorService.getDirty(resources)); - - return dirty; - } - - isDirty(resource?: URI): boolean { - - // Check for dirty file - if (this._models.getAll(resource).some(model => model.isDirty())) { - return true; - } + isDirty(resource: URI): boolean { // Check for dirty untitled - return this.untitledTextEditorService.getDirty().some(dirty => !resource || dirty.toString() === resource.toString()); + if (resource.scheme === Schemas.untitled) { + const model = this.untitled.get(resource); + if (model) { + return model.isDirty(); + } + + return false; + } + + // Check for dirty file + return this.files.getAll(resource).some(model => model.isDirty()); } //#endregion - - dispose(): void { - - // Clear all caches - this._models.clear(); - - super.dispose(); - } } diff --git a/src/vs/workbench/services/textfile/common/saveSequenzializer.ts b/src/vs/workbench/services/textfile/common/saveSequenzializer.ts new file mode 100644 index 0000000000..20e294ceb6 --- /dev/null +++ b/src/vs/workbench/services/textfile/common/saveSequenzializer.ts @@ -0,0 +1,95 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +interface IPendingSave { + versionId: number; + promise: Promise; +} + +interface ISaveOperation { + promise: Promise; + promiseResolve: () => void; + promiseReject: (error: Error) => void; + run: () => Promise; +} + +export class SaveSequentializer { + private _pendingSave?: IPendingSave; + private _nextSave?: ISaveOperation; + + hasPendingSave(versionId?: number): boolean { + if (!this._pendingSave) { + return false; + } + + if (typeof versionId === 'number') { + return this._pendingSave.versionId === versionId; + } + + return !!this._pendingSave; + } + + get pendingSave(): Promise | undefined { + return this._pendingSave ? this._pendingSave.promise : undefined; + } + + setPending(versionId: number, promise: Promise): Promise { + this._pendingSave = { versionId, promise }; + + promise.then(() => this.donePending(versionId), () => this.donePending(versionId)); + + return promise; + } + + private donePending(versionId: number): void { + if (this._pendingSave && versionId === this._pendingSave.versionId) { + + // only set pending to done if the promise finished that is associated with that versionId + this._pendingSave = undefined; + + // schedule the next save now that we are free if we have any + this.triggerNextSave(); + } + } + + private triggerNextSave(): void { + if (this._nextSave) { + const saveOperation = this._nextSave; + this._nextSave = undefined; + + // Run next save and complete on the associated promise + saveOperation.run().then(saveOperation.promiseResolve, saveOperation.promiseReject); + } + } + + setNext(run: () => Promise): Promise { + + // this is our first next save, so we create associated promise with it + // so that we can return a promise that completes when the save operation + // has completed. + if (!this._nextSave) { + let promiseResolve: () => void; + let promiseReject: (error: Error) => void; + const promise = new Promise((resolve, reject) => { + promiseResolve = resolve; + promiseReject = reject; + }); + + this._nextSave = { + run, + promise, + promiseResolve: promiseResolve!, + promiseReject: promiseReject! + }; + } + + // we have a previous next save, just overwrite it + else { + this._nextSave.run = run; + } + + return this._nextSave.promise; + } +} diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index f66e99f977..5b75d5595d 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -5,34 +5,27 @@ import * as nls from 'vs/nls'; import { Emitter } from 'vs/base/common/event'; -import { guessMimeTypes } from 'vs/base/common/mime'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { URI } from 'vs/base/common/uri'; -import { isUndefinedOrNull, assertIsDefined } from 'vs/base/common/types'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ITextFileService, ModelState, ITextFileEditorModel, ISaveErrorHandler, ISaveParticipant, StateChange, ITextFileStreamContent, ILoadOptions, LoadReason, IResolvedTextFileEditorModel, ITextFileSaveOptions } from 'vs/workbench/services/textfile/common/textfiles'; +import { assertIsDefined } from 'vs/base/common/types'; +import { ITextFileService, ModelState, ITextFileEditorModel, ISaveErrorHandler, ISaveParticipant, ITextFileStreamContent, ILoadOptions, IResolvedTextFileEditorModel, ITextFileSaveOptions, LoadReason } from 'vs/workbench/services/textfile/common/textfiles'; import { EncodingMode, IRevertOptions, SaveReason } from 'vs/workbench/common/editor'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; -import { IFileService, FileOperationError, FileOperationResult, CONTENT_CHANGE_EVENT_BUFFER_DELAY, FileChangesEvent, FileChangeType, IFileStatWithMetadata, ETAG_DISABLED, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IFileService, FileOperationError, FileOperationResult, FileChangesEvent, FileChangeType, IFileStatWithMetadata, ETAG_DISABLED, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { RunOnceScheduler, timeout } from 'vs/base/common/async'; +import { timeout } from 'vs/base/common/async'; import { ITextBufferFactory } from 'vs/editor/common/model'; -import { hash } from 'vs/base/common/hash'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { toDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; -import { isEqual, isEqualOrParent, extname, basename, joinPath } from 'vs/base/common/resources'; +import { isEqual, basename } from 'vs/base/common/resources'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { Schemas } from 'vs/base/common/network'; -import { IWorkingCopyService, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { IFilesConfigurationService, IAutoSaveConfiguration } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { SaveSequentializer } from 'vs/workbench/services/textfile/common/saveSequenzializer'; -export interface IBackupMetaData { +interface IBackupMetaData { mtime: number; ctime: number; size: number; @@ -40,49 +33,46 @@ export interface IBackupMetaData { orphaned: boolean; } -type FileTelemetryDataFragment = { - mimeType: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - ext: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - path: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - reason?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; - whitelistedjson?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; -}; - -type TelemetryData = { - mimeType: string; - ext: string; - path: number; - reason?: number; - whitelistedjson?: string; -}; - /** * The text file editor model listens to changes to its underlying code editor model and saves these changes through the file service back to the disk. */ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFileEditorModel { - static DEFAULT_CONTENT_CHANGE_BUFFER_DELAY = CONTENT_CHANGE_EVENT_BUFFER_DELAY; - static DEFAULT_ORPHANED_CHANGE_BUFFER_DELAY = 100; - - static WHITELIST_JSON = ['package.json', 'package-lock.json', 'tsconfig.json', 'jsconfig.json', 'bower.json', '.eslintrc.json', 'tslint.json', 'composer.json']; - static WHITELIST_WORKSPACE_JSON = ['settings.json', 'extensions.json', 'tasks.json', 'launch.json']; - private static saveErrorHandler: ISaveErrorHandler; static setSaveErrorHandler(handler: ISaveErrorHandler): void { TextFileEditorModel.saveErrorHandler = handler; } private static saveParticipant: ISaveParticipant | null; static setSaveParticipant(handler: ISaveParticipant | null): void { TextFileEditorModel.saveParticipant = handler; } - private readonly _onDidContentChange = this._register(new Emitter()); - readonly onDidContentChange = this._onDidContentChange.event; + //#region Events - private readonly _onDidStateChange = this._register(new Emitter()); - readonly onDidStateChange = this._onDidStateChange.event; + private readonly _onDidChangeContent = this._register(new Emitter()); + readonly onDidChangeContent = this._onDidChangeContent.event; + + private readonly _onDidLoad = this._register(new Emitter()); + readonly onDidLoad = this._onDidLoad.event; + + private readonly _onDidSaveError = this._register(new Emitter()); + readonly onDidSaveError = this._onDidSaveError.event; + + private readonly _onDidSave = this._register(new Emitter()); + readonly onDidSave = this._onDidSave.event; + + private readonly _onDidRevert = this._register(new Emitter()); + readonly onDidRevert = this._onDidRevert.event; + + private readonly _onDidChangeEncoding = this._register(new Emitter()); + readonly onDidChangeEncoding = this._onDidChangeEncoding.event; + + private readonly _onDidChangeOrphaned = this._register(new Emitter()); + readonly onDidChangeOrphaned = this._onDidChangeOrphaned.event; private readonly _onDidChangeDirty = this._register(new Emitter()); readonly onDidChangeDirty = this._onDidChangeDirty.event; - readonly capabilities = WorkingCopyCapabilities.AutoSave; + //#endregion + + readonly capabilities = 0; private contentEncoding: string | undefined; // encoding as reported from disk @@ -92,16 +82,9 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil private lastResolvedFileStat: IFileStatWithMetadata | undefined; - private autoSaveAfterMillies: number | undefined; - private autoSaveAfterMilliesEnabled: boolean | undefined; - private readonly autoSaveDisposable = this._register(new MutableDisposable()); - private readonly saveSequentializer = new SaveSequentializer(); private lastSaveAttemptTime = 0; - private readonly contentChangeEventScheduler = this._register(new RunOnceScheduler(() => this._onDidContentChange.fire(StateChange.CONTENT_CHANGE), TextFileEditorModel.DEFAULT_CONTENT_CHANGE_BUFFER_DELAY)); - private readonly orphanedChangeEventScheduler = this._register(new RunOnceScheduler(() => this._onDidStateChange.fire(StateChange.ORPHANED_CHANGE), TextFileEditorModel.DEFAULT_ORPHANED_CHANGE_BUFFER_DELAY)); - private dirty = false; private inConflictMode = false; private inOrphanMode = false; @@ -116,20 +99,14 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil @IModeService modeService: IModeService, @IModelService modelService: IModelService, @IFileService private readonly fileService: IFileService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @ITelemetryService private readonly telemetryService: ITelemetryService, @ITextFileService private readonly textFileService: ITextFileService, @IBackupFileService private readonly backupFileService: IBackupFileService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, - @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @ILogService private readonly logService: ILogService, @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService ) { super(modelService, modeService); - this.updateAutoSaveConfiguration(filesConfigurationService.getAutoSaveConfiguration()); - // Make known to working copy service this._register(this.workingCopyService.registerWorkingCopy(this)); @@ -138,20 +115,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil private registerListeners(): void { this._register(this.fileService.onFileChanges(e => this.onFileChanges(e))); - this._register(this.filesConfigurationService.onAutoSaveConfigurationChange(config => this.updateAutoSaveConfiguration(config))); this._register(this.filesConfigurationService.onFilesAssociationChange(e => this.onFilesAssociationChange())); - this._register(this.onDidStateChange(e => this.onStateChange(e))); - } - - private onStateChange(e: StateChange): void { - if (e === StateChange.REVERTED) { - - // Cancel any content change event promises as they are no longer valid. - this.contentChangeEventScheduler.cancel(); - - // Refire state change reverted events as content change events - this._onDidContentChange.fire(StateChange.REVERTED); - } } private async onFileChanges(e: FileChangesEvent): Promise { @@ -202,17 +166,10 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil private setOrphaned(orphaned: boolean): void { if (this.inOrphanMode !== orphaned) { this.inOrphanMode = orphaned; - this.orphanedChangeEventScheduler.schedule(); + this._onDidChangeOrphaned.fire(); } } - private updateAutoSaveConfiguration(config: IAutoSaveConfiguration): void { - const autoSaveAfterMilliesEnabled = (typeof config.autoSaveDelay === 'number') && config.autoSaveDelay > 0; - - this.autoSaveAfterMilliesEnabled = autoSaveAfterMilliesEnabled; - this.autoSaveAfterMillies = autoSaveAfterMilliesEnabled ? config.autoSaveDelay : undefined; - } - private onFilesAssociationChange(): void { if (!this.isResolved()) { return; @@ -230,6 +187,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil this.preferredMode = mode; } + //#region Backup + async backup(target = this.resource): Promise { if (this.isResolved()) { @@ -253,17 +212,18 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return this.backupFileService.hasBackupSync(this.resource, this.versionId); } + //#endregion + + //#region Revert + async revert(options?: IRevertOptions): Promise { if (!this.isResolved()) { return false; } - // Cancel any running auto-save - this.autoSaveDisposable.clear(); - // Unset flags const wasDirty = this.dirty; - const undo = this.setDirty(false); + const undo = this.doSetDirty(false); // Force read from disk unless reverting soft const softUndo = options?.soft; @@ -280,7 +240,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } // Emit file change event - this._onDidStateChange.fire(StateChange.REVERTED); + this._onDidRevert.fire(); // Emit dirty change event if (wasDirty) { @@ -290,14 +250,18 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return true; } + //#endregion + + //#region Load + async load(options?: ILoadOptions): Promise { - this.logService.trace('load() - enter', this.resource); + this.logService.trace('[text file model] load() - enter', this.resource.toString()); // 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. if (this.dirty || this.saveSequentializer.hasPendingSave()) { - this.logService.trace('load() - exit - without loading because model is dirty or being saved', this.resource); + this.logService.trace('[text file model] load() - exit - without loading because model is dirty or being saved', this.resource.toString()); return this; } @@ -314,7 +278,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil try { return await this.loadFromBackup(backup, options); } catch (error) { - this.logService.error(error); // ignore error and continue to load as file below + this.logService.error('[text file model] load()', error); // ignore error and continue to load as file below } } } @@ -395,7 +359,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Guard against the model having changed in the meantime if (currentVersionId === this.versionId) { - this.setDirty(false); // Ensure we are not tracking a stale state + this.doSetDirty(false); // Ensure we are not tracking a stale state } return this; @@ -414,7 +378,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private loadFromContent(content: ITextFileStreamContent, options?: ILoadOptions, fromBackup?: boolean): TextFileEditorModel { - this.logService.trace('load() - resolved content', this.resource); + this.logService.trace('[text file model] load() - resolved content', this.resource.toString()); // Update our resolved disk stat model this.updateLastResolvedFileStat({ @@ -437,7 +401,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil if (this.preferredEncoding) { this.updatePreferredEncoding(this.contentEncoding); // make sure to reflect the real encoding of the file (never out of sync) } else if (oldEncoding !== this.contentEncoding) { - this._onDidStateChange.fire(StateChange.ENCODING); + this._onDidChangeEncoding.fire(); } // Update Existing Model @@ -450,42 +414,26 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil this.doCreateTextModel(content.resource, content.value, !!fromBackup); } - // Telemetry: We log the fileGet telemetry event after the model has been loaded to ensure a good mimetype - const settingsType = this.getTypeIfSettings(); - if (settingsType) { - type SettingsReadClassification = { - settingsType: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - }; - - this.telemetryService.publicLog2<{ settingsType: string }, SettingsReadClassification>('settingsRead', { settingsType }); // Do not log read to user settings.json and .vscode folder as a fileGet event as it ruins our JSON usage data - } else { - type FileGetClassification = {} & FileTelemetryDataFragment; - - this.telemetryService.publicLog2('fileGet', this.getTelemetryData(options?.reason ?? LoadReason.OTHER)); - } + // Emit as event + this._onDidLoad.fire(options?.reason ?? LoadReason.OTHER); return this; } private doCreateTextModel(resource: URI, value: ITextBufferFactory, fromBackup: boolean): void { - this.logService.trace('load() - created text editor model', this.resource); + this.logService.trace('[text file model] load() - created text editor model', this.resource.toString()); // Create model this.createTextEditorModel(value, resource, this.preferredMode); // We restored a backup so we have to set the model as being dirty - // We also want to trigger auto save if it is enabled to simulate the exact same behaviour - // you would get if manually making the model dirty (fixes https://github.com/Microsoft/vscode/issues/16977) if (fromBackup) { this.doMakeDirty(); - if (this.autoSaveAfterMilliesEnabled) { - this.doAutoSave(this.versionId); - } } // Ensure we are not tracking a stale state else { - this.setDirty(false); + this.doSetDirty(false); } // Model Listeners @@ -493,10 +441,10 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private doUpdateTextModel(value: ITextBufferFactory): void { - this.logService.trace('load() - updated text editor model', this.resource); + this.logService.trace('[text file model] load() - updated text editor model', this.resource.toString()); // Ensure we are not tracking a stale state - this.setDirty(false); + this.doSetDirty(false); // Update model value in a block that ignores model content change events this.blockModelContentChange = true; @@ -523,11 +471,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private onModelContentChanged(): void { - this.logService.trace(`onModelContentChanged() - enter`, this.resource); + this.logService.trace(`[text file model] onModelContentChanged() - enter`, this.resource.toString()); // In any case increment the version id because it tracks the textual content state of the model at all times this.versionId++; - this.logService.trace(`onModelContentChanged() - new versionId ${this.versionId}`, this.resource); + this.logService.trace(`[text file model] onModelContentChanged() - new versionId ${this.versionId}`, this.resource.toString()); // Ignore if blocking model changes if (this.blockModelContentChange) { @@ -536,40 +484,35 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // The contents changed as a matter of Undo and the version reached matches the saved one // In this case we clear the dirty flag and emit a SAVED event to indicate this state. - // Note: we currently only do this check when auto-save is turned off because there you see - // a dirty indicator that you want to get rid of when undoing to the saved version. - if (!this.autoSaveAfterMilliesEnabled && this.isResolved() && this.textEditorModel.getAlternativeVersionId() === this.bufferSavedVersionId) { - this.logService.trace('onModelContentChanged() - model content changed back to last saved version', this.resource); + if (this.isResolved() && this.textEditorModel.getAlternativeVersionId() === this.bufferSavedVersionId) { + this.logService.trace('[text file model] onModelContentChanged() - model content changed back to last saved version', this.resource.toString()); // Clear flags const wasDirty = this.dirty; - this.setDirty(false); + this.doSetDirty(false); // Emit event if (wasDirty) { - this._onDidStateChange.fire(StateChange.REVERTED); + this._onDidRevert.fire(); this._onDidChangeDirty.fire(); } + } else { + this.logService.trace('[text file model] onModelContentChanged() - model content changed and marked as dirty', this.resource.toString()); - return; + // Mark as dirty + this.doMakeDirty(); } - this.logService.trace('onModelContentChanged() - model content changed and marked as dirty', this.resource); + // Emit as event + this._onDidChangeContent.fire(); + } - // Mark as dirty - this.doMakeDirty(); + //#endregion - // Start auto save process unless we are in conflict resolution mode and unless it is disabled - if (this.autoSaveAfterMilliesEnabled) { - if (!this.inConflictMode) { - this.doAutoSave(this.versionId); - } else { - this.logService.trace('makeDirty() - prevented save because we are in conflict resolution mode', this.resource); - } - } + //#region Dirty - // Handle content change events - this.contentChangeEventScheduler.schedule(); + isDirty(): this is IResolvedTextFileEditorModel { + return this.dirty; } makeDirty(): void { @@ -584,57 +527,78 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Track dirty state and version id const wasDirty = this.dirty; - this.setDirty(true); + this.doSetDirty(true); // Emit as Event if we turned dirty if (!wasDirty) { - this._onDidStateChange.fire(StateChange.DIRTY); this._onDidChangeDirty.fire(); } } - private doAutoSave(versionId: number): void { - this.logService.trace(`doAutoSave() - enter for versionId ${versionId}`, this.resource); + private doSetDirty(dirty: boolean): () => void { + const wasDirty = this.dirty; + const wasInConflictMode = this.inConflictMode; + const wasInErrorMode = this.inErrorMode; + const oldBufferSavedVersionId = this.bufferSavedVersionId; - // Cancel any currently running auto saves to make this the one that succeeds - this.autoSaveDisposable.clear(); + if (!dirty) { + this.dirty = false; + this.inConflictMode = false; + this.inErrorMode = false; + this.updateSavedVersionId(); + } else { + this.dirty = true; + } - // Create new save timer and store it for disposal as needed - const handle = setTimeout(() => { - - // Clear the timeout now that we are running - this.autoSaveDisposable.clear(); - - // Only trigger save if the version id has not changed meanwhile - if (versionId === this.versionId) { - this.doSave(versionId, { reason: SaveReason.AUTO }); // Very important here to not return the promise because if the timeout promise is canceled it will bubble up the error otherwise - do not change - } - }, this.autoSaveAfterMillies); - - this.autoSaveDisposable.value = toDisposable(() => clearTimeout(handle)); + // Return function to revert this call + return () => { + this.dirty = wasDirty; + this.inConflictMode = wasInConflictMode; + this.inErrorMode = wasInErrorMode; + this.bufferSavedVersionId = oldBufferSavedVersionId; + }; } + //#endregion + + //#region Save + async save(options: ITextFileSaveOptions = Object.create(null)): Promise { if (!this.isResolved()) { return false; } - this.logService.trace('save() - enter', this.resource); + if (this.isReadonly()) { + this.logService.trace('[text file model] save() - ignoring request for readonly resource', this.resource.toString()); - // Cancel any currently running auto saves to make this the one that succeeds - this.autoSaveDisposable.clear(); + return false; // if model is readonly we do not attempt to save at all + } - await this.doSave(this.versionId, options); + if ( + (this.hasState(ModelState.CONFLICT) || this.hasState(ModelState.ERROR)) && + (options.reason === SaveReason.AUTO || options.reason === SaveReason.FOCUS_CHANGE || options.reason === SaveReason.WINDOW_CHANGE) + ) { + this.logService.trace('[text file model] save() - ignoring auto save request for model that is in conflict or error', this.resource.toString()); + + return false; // if model is in save conflict or error, do not save unless save reason is explicit + } + + this.logService.trace('[text file model] save() - enter', this.resource.toString()); + + await this.doSave(options); + + this.logService.trace('[text file model] save() - exit', this.resource.toString()); return true; } - private doSave(versionId: number, options: ITextFileSaveOptions): Promise { - if (isUndefinedOrNull(options.reason)) { + private doSave(options: ITextFileSaveOptions): Promise { + if (typeof options.reason !== 'number') { options.reason = SaveReason.EXPLICIT; } - this.logService.trace(`doSave(${versionId}) - enter with versionId ' + versionId`, this.resource); + let versionId = this.versionId; + this.logService.trace(`[text file model] doSave(${versionId}) - enter with versionId ${versionId}`, this.resource.toString()); // Lookup any running pending save for this versionId and return it if found // @@ -642,20 +606,16 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // while the save was not yet finished to disk // if (this.saveSequentializer.hasPendingSave(versionId)) { - this.logService.trace(`doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`, this.resource); + this.logService.trace(`[text file model] doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`, this.resource.toString()); return this.saveSequentializer.pendingSave || Promise.resolve(); } - // Return early if not dirty (unless forced) or version changed meanwhile + // Return early if not dirty (unless forced) // - // Scenario A: user invoked save action even though the model is not dirty - // Scenario B: auto save was triggered for a certain change by the user but meanwhile the user changed - // the contents and the version for which auto save was started is no longer the latest. - // Thus we avoid spawning multiple auto saves and only take the latest. - // - if ((!options.force && !this.dirty) || versionId !== this.versionId) { - this.logService.trace(`doSave(${versionId}) - exit - because not dirty and/or versionId is different (this.isDirty: ${this.dirty}, this.versionId: ${this.versionId})`, this.resource); + // Scenario: user invoked save action even though the model is not dirty + if (!options.force && !this.dirty) { + this.logService.trace(`[text file model] doSave(${versionId}) - exit - because not dirty and/or versionId is different (this.isDirty: ${this.dirty}, this.versionId: ${this.versionId})`, this.resource.toString()); return Promise.resolve(); } @@ -669,15 +629,15 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // while the first save has not returned yet. // if (this.saveSequentializer.hasPendingSave()) { - this.logService.trace(`doSave(${versionId}) - exit - because busy saving`, this.resource); + this.logService.trace(`[text file model] doSave(${versionId}) - exit - because busy saving`, this.resource.toString()); // Register this as the next upcoming save and return - return this.saveSequentializer.setNext(() => this.doSave(this.versionId /* make sure to use latest version id here */, options)); + return this.saveSequentializer.setNext(() => this.doSave(options)); } // Push all edit operations to the undo stack so that the user has a chance to - // Ctrl+Z back to the saved version. We only do this when auto-save is turned off - if (!this.autoSaveAfterMilliesEnabled && this.isResolved()) { + // Ctrl+Z back to the saved version. + if (this.isResolved()) { this.textEditorModel.pushStackElement(); } @@ -723,7 +683,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // - the model is not in orphan mode (because in that case we know the file does not exist on disk) // - the model version did not change due to save participants running if (options.force && !this.dirty && !this.inOrphanMode && options.reason === SaveReason.EXPLICIT && versionId === newVersionId) { - return this.doTouch(newVersionId); + return this.doTouch(newVersionId, options.reason); } // update versionId with its new value (if pre-save changes happened) @@ -737,49 +697,34 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Save to Disk // mark the save operation as currently pending with the versionId (it might have changed from a save participant triggering) - this.logService.trace(`doSave(${versionId}) - before write()`, this.resource); + this.logService.trace(`[text file model] doSave(${versionId}) - before write()`, this.resource.toString()); const lastResolvedFileStat = assertIsDefined(this.lastResolvedFileStat); return this.saveSequentializer.setPending(newVersionId, this.textFileService.write(lastResolvedFileStat.resource, this.createSnapshot(), { overwriteReadonly: options.overwriteReadonly, overwriteEncoding: options.overwriteEncoding, mtime: lastResolvedFileStat.mtime, encoding: this.getEncoding(), - etag: (options.ignoreModifiedSince || !this.filesConfigurationService.preventSaveConflicts(lastResolvedFileStat.resource)) ? ETAG_DISABLED : lastResolvedFileStat.etag, + etag: (options.ignoreModifiedSince || !this.filesConfigurationService.preventSaveConflicts(lastResolvedFileStat.resource, this.getMode())) ? ETAG_DISABLED : lastResolvedFileStat.etag, writeElevated: options.writeElevated }).then(stat => { - this.logService.trace(`doSave(${versionId}) - after write()`, this.resource); + this.logService.trace(`[text file model] doSave(${versionId}) - after write()`, this.resource.toString()); // Update dirty state unless model has changed meanwhile if (versionId === this.versionId) { - this.logService.trace(`doSave(${versionId}) - setting dirty to false because versionId did not change`, this.resource); - this.setDirty(false); + this.logService.trace(`[text file model] doSave(${versionId}) - setting dirty to false because versionId did not change`, this.resource.toString()); + this.doSetDirty(false); } else { - this.logService.trace(`doSave(${versionId}) - not setting dirty to false because versionId did change meanwhile`, this.resource); + this.logService.trace(`[text file model] doSave(${versionId}) - not setting dirty to false because versionId did change meanwhile`, this.resource.toString()); } // Updated resolved stat with updated stat this.updateLastResolvedFileStat(stat); - // Cancel any content change event promises as they are no longer valid - this.contentChangeEventScheduler.cancel(); - // Emit Events - this._onDidStateChange.fire(StateChange.SAVED); + this._onDidSave.fire(options.reason ?? SaveReason.EXPLICIT); this._onDidChangeDirty.fire(); - - // Telemetry - const settingsType = this.getTypeIfSettings(); - if (settingsType) { - type SettingsWrittenClassification = { - settingsType: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - }; - this.telemetryService.publicLog2<{ settingsType: string }, SettingsWrittenClassification>('settingsWritten', { settingsType }); // Do not log write to user settings.json and .vscode folder as a filePUT event as it ruins our JSON usage data - } else { - type FilePutClassfication = {} & FileTelemetryDataFragment; - this.telemetryService.publicLog2('filePUT', this.getTelemetryData(options.reason)); - } }, error => { - this.logService.error(`doSave(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource); + this.logService.error(`[text file model] doSave(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource.toString()); // Flag as error state in the model this.inErrorMode = true; @@ -793,67 +738,12 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil this.onSaveError(error); // Emit as event - this._onDidStateChange.fire(StateChange.SAVE_ERROR); + this._onDidSaveError.fire(); })); })); } - private getTypeIfSettings(): string { - if (extname(this.resource) !== '.json') { - return ''; - } - - // Check for global settings file - if (isEqual(this.resource, this.environmentService.settingsResource)) { - return 'global-settings'; - } - - // Check for keybindings file - if (isEqual(this.resource, this.environmentService.keybindingsResource)) { - return 'keybindings'; - } - - // Check for snippets - if (isEqualOrParent(this.resource, joinPath(this.environmentService.userRoamingDataHome, 'snippets'))) { - return 'snippets'; - } - - // Check for workspace settings file - const folders = this.contextService.getWorkspace().folders; - for (const folder of folders) { - // {{SQL CARBON EDIT}} - if (isEqualOrParent(this.resource, folder.toResource('.azuredatastudio'))) { - const filename = basename(this.resource); - if (TextFileEditorModel.WHITELIST_WORKSPACE_JSON.indexOf(filename) > -1) { - // {{SQL CARBON EDIT}} - return `.azuredatastudio/${filename}`; - } - } - } - - return ''; - } - - private getTelemetryData(reason: number | undefined): TelemetryData { - const ext = extname(this.resource); - const fileName = basename(this.resource); - const path = this.resource.scheme === Schemas.file ? this.resource.fsPath : this.resource.path; - const telemetryData = { - mimeType: guessMimeTypes(this.resource).join(', '), - ext, - path: hash(path), - reason, - whitelistedjson: undefined as string | undefined - }; - - if (ext === '.json' && TextFileEditorModel.WHITELIST_JSON.indexOf(fileName) > -1) { - telemetryData['whitelistedjson'] = fileName; - } - - return telemetryData; - } - - private doTouch(versionId: number): Promise { + private doTouch(versionId: number, reason: SaveReason): Promise { if (!this.isResolved()) { return Promise.resolve(); } @@ -869,35 +759,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil this.updateLastResolvedFileStat(stat); // Emit File Saved Event - this._onDidStateChange.fire(StateChange.SAVED); + this._onDidSave.fire(reason); }, error => onUnexpectedError(error) /* just log any error but do not notify the user since the file was not dirty */)); } - private setDirty(dirty: boolean): () => void { - const wasDirty = this.dirty; - const wasInConflictMode = this.inConflictMode; - const wasInErrorMode = this.inErrorMode; - const oldBufferSavedVersionId = this.bufferSavedVersionId; - - if (!dirty) { - this.dirty = false; - this.inConflictMode = false; - this.inErrorMode = false; - this.updateSavedVersionId(); - } else { - this.dirty = true; - } - - // Return function to revert this call - return () => { - this.dirty = wasDirty; - this.inConflictMode = wasInConflictMode; - this.inErrorMode = wasInErrorMode; - this.bufferSavedVersionId = oldBufferSavedVersionId; - }; - } - private updateSavedVersionId(): void { // we remember the models alternate version id to remember when the version // of the model matches with the saved version on disk. we need to keep this @@ -928,16 +794,19 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Prepare handler if (!TextFileEditorModel.saveErrorHandler) { - TextFileEditorModel.setSaveErrorHandler(this.instantiationService.createInstance(DefaultSaveErrorHandler)); + const notificationService = this.notificationService; + TextFileEditorModel.setSaveErrorHandler({ + onSaveError(error: Error, model: TextFileEditorModel): void { + notificationService.error(nls.localize('genericSaveError', "Failed to save '{0}': {1}", basename(model.resource), toErrorMessage(error, false))); + } + }); } // Handle TextFileEditorModel.saveErrorHandler.onSaveError(error, this); } - isDirty(): boolean { // {{SQL CARBON EDIT}} strict-null-check - return this.dirty; - } + //#endregion getLastSaveAttemptTime(): number { return this.lastSaveAttemptTime; @@ -955,13 +824,13 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return this.inOrphanMode; case ModelState.PENDING_SAVE: return this.saveSequentializer.hasPendingSave(); - case ModelState.PENDING_AUTO_SAVE: - return !!this.autoSaveDisposable.value; case ModelState.SAVED: return !this.dirty; } } + getMode(this: IResolvedTextFileEditorModel): string; + getMode(): string | undefined; getMode(): string | undefined { if (this.textEditorModel) { return this.textEditorModel.getModeId(); @@ -970,6 +839,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return this.preferredMode; } + //#region Encoding + getEncoding(): string | undefined { return this.preferredEncoding || this.contentEncoding; } @@ -1019,7 +890,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil this.preferredEncoding = encoding; // Emit - this._onDidStateChange.fire(StateChange.ENCODING); + this._onDidChangeEncoding.fire(); } private isNewEncoding(encoding: string | undefined): boolean { @@ -1034,7 +905,9 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return true; } - isResolved(): boolean { // {{SQL CARBON EDIT}} @anthonydresser strict-null-check + //#endregion + + isResolved(): this is IResolvedTextFileEditorModel { return !!this.textEditorModel; } @@ -1059,103 +932,3 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil super.dispose(); } } - -interface IPendingSave { - versionId: number; - promise: Promise; -} - -interface ISaveOperation { - promise: Promise; - promiseResolve: () => void; - promiseReject: (error: Error) => void; - run: () => Promise; -} - -export class SaveSequentializer { - private _pendingSave?: IPendingSave; - private _nextSave?: ISaveOperation; - - hasPendingSave(versionId?: number): boolean { - if (!this._pendingSave) { - return false; - } - - if (typeof versionId === 'number') { - return this._pendingSave.versionId === versionId; - } - - return !!this._pendingSave; - } - - get pendingSave(): Promise | undefined { - return this._pendingSave ? this._pendingSave.promise : undefined; - } - - setPending(versionId: number, promise: Promise): Promise { - this._pendingSave = { versionId, promise }; - - promise.then(() => this.donePending(versionId), () => this.donePending(versionId)); - - return promise; - } - - private donePending(versionId: number): void { - if (this._pendingSave && versionId === this._pendingSave.versionId) { - - // only set pending to done if the promise finished that is associated with that versionId - this._pendingSave = undefined; - - // schedule the next save now that we are free if we have any - this.triggerNextSave(); - } - } - - private triggerNextSave(): void { - if (this._nextSave) { - const saveOperation = this._nextSave; - this._nextSave = undefined; - - // Run next save and complete on the associated promise - saveOperation.run().then(saveOperation.promiseResolve, saveOperation.promiseReject); - } - } - - setNext(run: () => Promise): Promise { - - // this is our first next save, so we create associated promise with it - // so that we can return a promise that completes when the save operation - // has completed. - if (!this._nextSave) { - let promiseResolve: () => void; - let promiseReject: (error: Error) => void; - const promise = new Promise((resolve, reject) => { - promiseResolve = resolve; - promiseReject = reject; - }); - - this._nextSave = { - run, - promise, - promiseResolve: promiseResolve!, - promiseReject: promiseReject! - }; - } - - // we have a previous next save, just overwrite it - else { - this._nextSave.run = run; - } - - return this._nextSave.promise; - } -} - -class DefaultSaveErrorHandler implements ISaveErrorHandler { - - constructor(@INotificationService private readonly notificationService: INotificationService) { } - - onSaveError(error: Error, model: TextFileEditorModel): void { - this.notificationService.error(nls.localize('genericSaveError', "Failed to save '{0}': {1}", basename(model.resource), toErrorMessage(error, false))); - } -} diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts index 7e158be87b..815e886ef3 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts @@ -3,11 +3,11 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event, Emitter } from 'vs/base/common/event'; +import { Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; -import { dispose, IDisposable, Disposable } from 'vs/base/common/lifecycle'; -import { ITextFileEditorModel, ITextFileEditorModelManager, TextFileModelChangeEvent, StateChange, IModelLoadOrCreateOptions } from 'vs/workbench/services/textfile/common/textfiles'; +import { dispose, IDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { ITextFileEditorModel, ITextFileEditorModelManager, IModelLoadOrCreateOptions, ITextFileModelLoadEvent, ITextFileModelSaveEvent } from 'vs/workbench/services/textfile/common/textfiles'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ResourceMap } from 'vs/base/common/map'; @@ -18,70 +18,30 @@ import { onUnexpectedError } from 'vs/base/common/errors'; export class TextFileEditorModelManager extends Disposable implements ITextFileEditorModelManager { - private readonly _onModelDisposed = this._register(new Emitter()); - readonly onModelDisposed = this._onModelDisposed.event; + private readonly _onDidLoad = this._register(new Emitter()); + readonly onDidLoad = this._onDidLoad.event; - private readonly _onModelContentChanged = this._register(new Emitter()); - readonly onModelContentChanged = this._onModelContentChanged.event; + private readonly _onDidChangeDirty = this._register(new Emitter()); + readonly onDidChangeDirty = this._onDidChangeDirty.event; - private readonly _onModelDirty = this._register(new Emitter()); - readonly onModelDirty = this._onModelDirty.event; + private readonly _onDidSaveError = this._register(new Emitter()); + readonly onDidSaveError = this._onDidSaveError.event; - private readonly _onModelSaveError = this._register(new Emitter()); - readonly onModelSaveError = this._onModelSaveError.event; + private readonly _onDidSave = this._register(new Emitter()); + readonly onDidSave = this._onDidSave.event; - private readonly _onModelSaved = this._register(new Emitter()); - readonly onModelSaved = this._onModelSaved.event; + private readonly _onDidRevert = this._register(new Emitter()); + readonly onDidRevert = this._onDidRevert.event; - private readonly _onModelReverted = this._register(new Emitter()); - readonly onModelReverted = this._onModelReverted.event; + private readonly _onDidChangeEncoding = this._register(new Emitter()); + readonly onDidChangeEncoding = this._onDidChangeEncoding.event; - private readonly _onModelEncodingChanged = this._register(new Emitter()); - readonly onModelEncodingChanged = this._onModelEncodingChanged.event; + private readonly _onDidChangeOrphaned = this._register(new Emitter()); + readonly onDidChangeOrphaned = this._onDidChangeOrphaned.event; - private readonly _onModelOrphanedChanged = this._register(new Emitter()); - readonly onModelOrphanedChanged = this._onModelOrphanedChanged.event; - - private _onModelsDirty: Event> | undefined; - get onModelsDirty(): Event> { - if (!this._onModelsDirty) { - this._onModelsDirty = this.debounce(this.onModelDirty); - } - - return this._onModelsDirty; - } - - private _onModelsSaveError: Event> | undefined; - get onModelsSaveError(): Event> { - if (!this._onModelsSaveError) { - this._onModelsSaveError = this.debounce(this.onModelSaveError); - } - - return this._onModelsSaveError; - } - - private _onModelsSaved: Event> | undefined; - get onModelsSaved(): Event> { - if (!this._onModelsSaved) { - this._onModelsSaved = this.debounce(this.onModelSaved); - } - - return this._onModelsSaved; - } - - private _onModelsReverted: Event> | undefined; - get onModelsReverted(): Event> { - if (!this._onModelsReverted) { - this._onModelsReverted = this.debounce(this.onModelReverted); - } - - return this._onModelsReverted; - } - - private readonly mapResourceToDisposeListener = new ResourceMap(); - private readonly mapResourceToStateChangeListener = new ResourceMap(); - private readonly mapResourceToModelContentChangeListener = new ResourceMap(); private readonly mapResourceToModel = new ResourceMap(); + private readonly mapResourceToModelListeners = new ResourceMap(); + private readonly mapResourceToDisposeListener = new ResourceMap(); private readonly mapResourceToPendingModelLoaders = new ResourceMap>(); private readonly modelLoadQueue = new ResourceQueue(); @@ -128,26 +88,11 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE } } - private debounce(event: Event): Event { - return Event.debounce(event, (prev, cur) => { - if (!prev) { - prev = [cur]; - } else { - prev.push(cur); - } - return prev; - }, this.debounceDelay()); - } - - protected debounceDelay(): number { - return 250; - } - get(resource: URI): ITextFileEditorModel | undefined { return this.mapResourceToModel.get(resource); } - async loadOrCreate(resource: URI, options?: IModelLoadOrCreateOptions): Promise { + async resolve(resource: URI, options?: IModelLoadOrCreateOptions): Promise { // Return early if model is currently being loaded const pendingLoad = this.mapResourceToPendingModelLoaders.get(resource); @@ -182,35 +127,17 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE const newModel = model = this.instantiationService.createInstance(TextFileEditorModel, resource, options ? options.encoding : undefined, options ? options.mode : undefined); modelPromise = model.load(options); - // Install state change listener - this.mapResourceToStateChangeListener.set(resource, model.onDidStateChange(state => { - const event = new TextFileModelChangeEvent(newModel, state); - switch (state) { - case StateChange.DIRTY: - this._onModelDirty.fire(event); - break; - case StateChange.SAVE_ERROR: - this._onModelSaveError.fire(event); - break; - case StateChange.SAVED: - this._onModelSaved.fire(event); - break; - case StateChange.REVERTED: - this._onModelReverted.fire(event); - break; - case StateChange.ENCODING: - this._onModelEncodingChanged.fire(event); - break; - case StateChange.ORPHANED_CHANGE: - this._onModelOrphanedChanged.fire(event); - break; - } - })); + // Install model listeners + const listeners = new DisposableStore(); + listeners.add(model.onDidLoad(reason => this._onDidLoad.fire({ model: newModel, reason }))); + listeners.add(model.onDidChangeDirty(() => this._onDidChangeDirty.fire(newModel))); + listeners.add(model.onDidSaveError(() => this._onDidSaveError.fire(newModel))); + listeners.add(model.onDidSave(reason => this._onDidSave.fire({ model: newModel, reason }))); + listeners.add(model.onDidRevert(() => this._onDidRevert.fire(newModel))); + listeners.add(model.onDidChangeEncoding(() => this._onDidChangeEncoding.fire(newModel))); + listeners.add(model.onDidChangeOrphaned(() => this._onDidChangeOrphaned.fire(newModel))); - // Install model content change listener - this.mapResourceToModelContentChangeListener.set(resource, model.onDidContentChange(e => { - this._onModelContentChanged.fire(new TextFileModelChangeEvent(newModel, e)); - })); + this.mapResourceToModelListeners.set(resource, listeners); } // Store pending loads to avoid race conditions @@ -224,7 +151,7 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE // Model can be dirty if a backup was restored, so we make sure to have this event delivered if (resolvedModel.isDirty()) { - this._onModelDirty.fire(new TextFileModelChangeEvent(resolvedModel, StateChange.DIRTY)); + this._onDidChangeDirty.fire(resolvedModel); } // Remove from pending loads @@ -281,10 +208,7 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE // store in cache but remove when model gets disposed this.mapResourceToModel.set(resource, model); - this.mapResourceToDisposeListener.set(resource, model.onDispose(() => { - this.remove(resource); - this._onModelDisposed.fire(resource); - })); + this.mapResourceToDisposeListener.set(resource, model.onDispose(() => this.remove(resource))); } remove(resource: URI): void { @@ -296,16 +220,10 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE this.mapResourceToDisposeListener.delete(resource); } - const stateChangeListener = this.mapResourceToStateChangeListener.get(resource); - if (stateChangeListener) { - dispose(stateChangeListener); - this.mapResourceToStateChangeListener.delete(resource); - } - - const modelContentChangeListener = this.mapResourceToModelContentChangeListener.get(resource); - if (modelContentChangeListener) { - dispose(modelContentChangeListener); - this.mapResourceToModelContentChangeListener.delete(resource); + const modelListener = this.mapResourceToModelListeners.get(resource); + if (modelListener) { + dispose(modelListener); + this.mapResourceToModelListeners.delete(resource); } } @@ -319,13 +237,9 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE this.mapResourceToDisposeListener.forEach(l => l.dispose()); this.mapResourceToDisposeListener.clear(); - // dispose the state change listeners - this.mapResourceToStateChangeListener.forEach(l => l.dispose()); - this.mapResourceToStateChangeListener.clear(); - - // dispose the model content change listeners - this.mapResourceToModelContentChangeListener.forEach(l => l.dispose()); - this.mapResourceToModelContentChangeListener.clear(); + // dispose the model change listeners + this.mapResourceToModelListeners.forEach(l => l.dispose()); + this.mapResourceToModelListeners.clear(); } disposeModel(model: TextFileEditorModel): void { diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index 40022a2a4f..4d523b9ce8 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -15,6 +15,7 @@ import { VSBuffer, VSBufferReadable } from 'vs/base/common/buffer'; import { isUndefinedOrNull } from 'vs/base/common/types'; import { isNative } from 'vs/base/common/platform'; import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IUntitledTextEditorModelManager } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; export const ITextFileService = createDecorator('textFileService'); @@ -33,9 +34,16 @@ export interface ITextFileService extends IDisposable { readonly onDidRunOperation: Event; /** - * Access to the manager of text file editor models providing further methods to work with them. + * Access to the manager of text file editor models providing further + * methods to work with them. */ - readonly models: ITextFileEditorModelManager; + readonly files: ITextFileEditorModelManager; + + /** + * Access to the manager of untitled text editor models providing further + * methods to work with them. + */ + readonly untitled: IUntitledTextEditorModelManager; /** * Helper to determine encoding for resources. @@ -45,18 +53,9 @@ export interface ITextFileService extends IDisposable { /** * A resource is dirty if it has unsaved changes or is an untitled file not yet saved. * - * @param resource the resource to check for being dirty. If it is not specified, will check for - * all dirty resources. + * @param resource the resource to check for being dirty */ - isDirty(resource?: URI): boolean; - - /** - * Returns all resources that are currently dirty matching the provided resources or all dirty resources. - * - * @param resources the resources to check for being dirty. If it is not specified, will check for - * all dirty resources. - */ - getDirty(resources?: URI[]): URI[]; + isDirty(resource: URI): boolean; /** * Saves the resource. @@ -77,15 +76,6 @@ export interface ITextFileService extends IDisposable { */ saveAs(resource: URI, targetResource?: URI, options?: ISaveOptions): Promise; - /** - * Saves the set of resources and returns a promise with the operation result. - * - * @param resources can be null to save all. - * @param includeUntitled to save all resources and optionally exclude untitled ones. - */ - saveAll(includeUntitled?: boolean, options?: ISaveOptions): Promise; - saveAll(resources: URI[], options?: ISaveOptions): Promise; - /** * Reverts the provided resource. * @@ -94,11 +84,6 @@ export interface ITextFileService extends IDisposable { */ revert(resource: URI, options?: IRevertOptions): Promise; - /** - * Reverts all the provided resources and returns a promise with the operation result. - */ - revertAll(resources?: URI[], options?: IRevertOptions): Promise; - /** * Create a file. If the file exists it will be overwritten with the contents if * the options enable to overwrite. @@ -257,11 +242,6 @@ export const enum ModelState { */ PENDING_SAVE, - /** - * A model is marked for being saved after a specific timeout. - */ - PENDING_AUTO_SAVE, - /** * A model is in conflict mode when changes cannot be saved because the * underlying file has changed. Models in conflict mode are always dirty. @@ -280,33 +260,6 @@ export const enum ModelState { ERROR } -export const enum StateChange { - DIRTY, - SAVING, - SAVE_ERROR, - SAVED, - REVERTED, - ENCODING, - CONTENT_CHANGE, - ORPHANED_CHANGE -} - -export class TextFileModelChangeEvent { - private _resource: URI; - - constructor(model: ITextFileEditorModel, private _kind: StateChange) { - this._resource = model.resource; - } - - get resource(): URI { - return this._resource; - } - - get kind(): StateChange { - return this._kind; - } -} - export interface ITextFileOperationResult { results: IResult[]; } @@ -367,9 +320,9 @@ export interface IModelLoadOrCreateOptions { /** * If the model was already loaded before, allows to trigger * a reload of it to fetch the latest contents: - * - async: loadOrCreate() will return immediately and trigger + * - async: resolve() will return immediately and trigger * a reload that will run in the background. - * - sync: loadOrCreate() will only return resolved when the + * - sync: resolve() will only return resolved when the * model has finished reloading. */ reload?: { @@ -382,28 +335,29 @@ export interface IModelLoadOrCreateOptions { allowBinary?: boolean; } +export interface ITextFileModelSaveEvent { + model: ITextFileEditorModel; + reason: SaveReason; +} + +export interface ITextFileModelLoadEvent { + model: ITextFileEditorModel; + reason: LoadReason; +} + export interface ITextFileEditorModelManager { - readonly onModelDisposed: Event; - readonly onModelContentChanged: Event; - readonly onModelEncodingChanged: Event; - - readonly onModelDirty: Event; - readonly onModelSaveError: Event; - readonly onModelSaved: Event; - readonly onModelReverted: Event; - readonly onModelOrphanedChanged: Event; - - readonly onModelsDirty: Event; - readonly onModelsSaveError: Event; - readonly onModelsSaved: Event; - readonly onModelsReverted: Event; + readonly onDidLoad: Event; + readonly onDidChangeDirty: Event; + readonly onDidSaveError: Event; + readonly onDidSave: Event; + readonly onDidRevert: Event; + readonly onDidChangeEncoding: Event; + readonly onDidChangeOrphaned: Event; get(resource: URI): ITextFileEditorModel | undefined; - getAll(resource?: URI): ITextFileEditorModel[]; - - loadOrCreate(resource: URI, options?: IModelLoadOrCreateOptions): Promise; + resolve(resource: URI, options?: IModelLoadOrCreateOptions): Promise; disposeModel(model: ITextFileEditorModel): void; } @@ -435,8 +389,13 @@ export interface ILoadOptions { export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport, IModeSupport, IWorkingCopy { - readonly onDidContentChange: Event; - readonly onDidStateChange: Event; + readonly onDidChangeContent: Event; + readonly onDidLoad: Event; + readonly onDidSaveError: Event; + readonly onDidSave: Event; + readonly onDidRevert: Event; + readonly onDidChangeEncoding: Event; + readonly onDidChangeOrphaned: Event; hasState(state: ModelState): boolean; diff --git a/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts b/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts index 7f887a40ff..186b184141 100644 --- a/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts +++ b/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts @@ -27,44 +27,37 @@ import { Readable } from 'stream'; import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; import { ITextSnapshot } from 'vs/editor/common/model'; import { nodeReadableToString, streamToNodeReadable, nodeStreamToVSBufferReadable } from 'vs/base/node/stream'; -import { IElectronService } from 'vs/platform/electron/node/electron'; import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { assign } from 'vs/base/common/objects'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; export class NativeTextFileService extends AbstractTextFileService { constructor( - @IWorkspaceContextService contextService: IWorkspaceContextService, @IFileService fileService: IFileService, @IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorService, @ILifecycleService lifecycleService: ILifecycleService, @IInstantiationService instantiationService: IInstantiationService, - @IModeService modeService: IModeService, @IModelService modelService: IModelService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, - @INotificationService notificationService: INotificationService, - @IBackupFileService backupFileService: IBackupFileService, @IHistoryService historyService: IHistoryService, @IDialogService dialogService: IDialogService, @IFileDialogService fileDialogService: IFileDialogService, @IEditorService editorService: IEditorService, @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, - @IElectronService private readonly electronService: IElectronService, @IProductService private readonly productService: IProductService, - @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService + @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, + @ITextModelService textModelService: ITextModelService ) { - super(contextService, fileService, untitledTextEditorService, lifecycleService, instantiationService, modeService, modelService, environmentService, notificationService, backupFileService, historyService, dialogService, fileDialogService, editorService, textResourceConfigurationService, filesConfigurationService); + super(fileService, untitledTextEditorService, lifecycleService, instantiationService, modelService, environmentService, historyService, dialogService, fileDialogService, editorService, textResourceConfigurationService, filesConfigurationService, textModelService); } private _encoding: EncodingOracle | undefined; @@ -310,10 +303,6 @@ export class NativeTextFileService extends AbstractTextFileService { }); }); } - - protected getWindowCount(): Promise { - return this.electronService.getWindowCount(); - } } export interface IEncodingOverride { diff --git a/src/vs/workbench/services/textfile/test/saveSequenzializer.test.ts b/src/vs/workbench/services/textfile/test/saveSequenzializer.test.ts new file mode 100644 index 0000000000..a34a70d208 --- /dev/null +++ b/src/vs/workbench/services/textfile/test/saveSequenzializer.test.ts @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { timeout } from 'vs/base/common/async'; +import { SaveSequentializer } from 'vs/workbench/services/textfile/common/saveSequenzializer'; + +suite('Files - SaveSequentializer', () => { + + test('SaveSequentializer - pending basics', async function () { + const sequentializer = new SaveSequentializer(); + + assert.ok(!sequentializer.hasPendingSave()); + assert.ok(!sequentializer.hasPendingSave(2323)); + assert.ok(!sequentializer.pendingSave); + + // pending removes itself after done + await sequentializer.setPending(1, Promise.resolve()); + assert.ok(!sequentializer.hasPendingSave()); + assert.ok(!sequentializer.hasPendingSave(1)); + assert.ok(!sequentializer.pendingSave); + + // pending removes itself after done (use timeout) + sequentializer.setPending(2, timeout(1)); + assert.ok(sequentializer.hasPendingSave()); + assert.ok(sequentializer.hasPendingSave(2)); + assert.ok(!sequentializer.hasPendingSave(1)); + assert.ok(sequentializer.pendingSave); + + await timeout(2); + assert.ok(!sequentializer.hasPendingSave()); + assert.ok(!sequentializer.hasPendingSave(2)); + assert.ok(!sequentializer.pendingSave); + }); + + test('SaveSequentializer - pending and next (finishes instantly)', async function () { + const sequentializer = new SaveSequentializer(); + + let pendingDone = false; + sequentializer.setPending(1, timeout(1).then(() => { pendingDone = true; return; })); + + // next finishes instantly + let nextDone = false; + const res = sequentializer.setNext(() => Promise.resolve(null).then(() => { nextDone = true; return; })); + + await res; + assert.ok(pendingDone); + assert.ok(nextDone); + }); + + test('SaveSequentializer - pending and next (finishes after timeout)', async function () { + const sequentializer = new SaveSequentializer(); + + let pendingDone = false; + sequentializer.setPending(1, timeout(1).then(() => { pendingDone = true; return; })); + + // next finishes after timeout + let nextDone = false; + const res = sequentializer.setNext(() => timeout(1).then(() => { nextDone = true; return; })); + + await res; + assert.ok(pendingDone); + assert.ok(nextDone); + }); + + test('SaveSequentializer - pending and multiple next (last one wins)', async function () { + const sequentializer = new SaveSequentializer(); + + let pendingDone = false; + sequentializer.setPending(1, timeout(1).then(() => { pendingDone = true; return; })); + + // next finishes after timeout + let firstDone = false; + let firstRes = sequentializer.setNext(() => timeout(2).then(() => { firstDone = true; return; })); + + let secondDone = false; + let secondRes = sequentializer.setNext(() => timeout(3).then(() => { secondDone = true; return; })); + + let thirdDone = false; + let thirdRes = sequentializer.setNext(() => timeout(4).then(() => { thirdDone = true; return; })); + + await Promise.all([firstRes, secondRes, thirdRes]); + assert.ok(pendingDone); + assert.ok(!firstDone); + assert.ok(!secondDone); + assert.ok(thirdDone); + }); +}); diff --git a/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts b/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts index 03bef53a6d..ac31ad76b0 100644 --- a/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts +++ b/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts @@ -6,8 +6,8 @@ import * as assert from 'assert'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { EncodingMode } from 'vs/workbench/common/editor'; -import { TextFileEditorModel, SaveSequentializer } from 'vs/workbench/services/textfile/common/textFileEditorModel'; -import { ITextFileService, ModelState, StateChange, snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; +import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; +import { ITextFileService, ModelState, snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; import { workbenchInstantiationService, TestTextFileService, createFileInput, TestFileService } from 'vs/workbench/test/workbenchTestServices'; import { toResource } from 'vs/base/test/common/utils'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; @@ -47,11 +47,39 @@ suite('Files - TextFileEditorModel', () => { }); teardown(() => { - (accessor.textFileService.models).clear(); + (accessor.textFileService.files).dispose(); TextFileEditorModel.setSaveParticipant(null); // reset any set participant accessor.fileService.setContent(content); }); + test('basic events', async function () { + const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + await model.load(); + + let onDidChangeContentCounter = 0; + model.onDidChangeContent(() => onDidChangeContentCounter++); + + let onDidChangeDirtyCounter = 0; + model.onDidChangeDirty(() => onDidChangeDirtyCounter++); + + model.textEditorModel?.setValue('bar'); + + assert.equal(onDidChangeContentCounter, 1); + assert.equal(onDidChangeDirtyCounter, 1); + + model.textEditorModel?.setValue('foo'); + + assert.equal(onDidChangeContentCounter, 2); + assert.equal(onDidChangeDirtyCounter, 1); + + await model.revert(); + + assert.equal(onDidChangeDirtyCounter, 2); + + model.dispose(); + }); + test('save', async function () { const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); @@ -67,10 +95,8 @@ suite('Files - TextFileEditorModel', () => { assert.equal(accessor.workingCopyService.isDirty(model.resource), true); let savedEvent = false; - model.onDidStateChange(e => { - if (e === StateChange.SAVED) { - savedEvent = true; - } + model.onDidSave(e => { + savedEvent = true; }); let workingCopyEvent = false; @@ -104,10 +130,8 @@ suite('Files - TextFileEditorModel', () => { await model.load(); let savedEvent = false; - model.onDidStateChange(e => { - if (e === StateChange.SAVED) { - savedEvent = true; - } + model.onDidSave(e => { + savedEvent = true; }); let workingCopyEvent = false; @@ -178,8 +202,12 @@ suite('Files - TextFileEditorModel', () => { const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index.txt'), 'utf8', undefined); assert.ok(model.hasState(ModelState.SAVED)); - model.onDidStateChange(e => { - assert.ok(e !== StateChange.DIRTY && e !== StateChange.SAVED); + model.onDidSave(e => { + assert.fail(); + }); + + model.onDidChangeDirty(e => { + assert.fail(); }); await model.load(); @@ -206,10 +234,8 @@ suite('Files - TextFileEditorModel', () => { const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); - model.onDidStateChange(e => { - if (e === StateChange.REVERTED) { - eventCounter++; - } + model.onDidRevert(e => { + eventCounter++; }); let workingCopyEvent = false; @@ -243,10 +269,8 @@ suite('Files - TextFileEditorModel', () => { const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); - model.onDidStateChange(e => { - if (e === StateChange.REVERTED) { - eventCounter++; - } + model.onDidRevert(e => { + eventCounter++; }); let workingCopyEvent = false; @@ -303,10 +327,8 @@ suite('Files - TextFileEditorModel', () => { await model.revert({ soft: true }); assert.ok(!model.isDirty()); - model.onDidStateChange(e => { - if (e === StateChange.DIRTY) { - eventCounter++; - } + model.onDidChangeDirty(e => { + eventCounter++; }); let workingCopyEvent = false; @@ -364,7 +386,6 @@ suite('Files - TextFileEditorModel', () => { assert.ok(m1Mtime > 0); assert.ok(m2Mtime > 0); - assert.ok(accessor.textFileService.isDirty()); assert.ok(accessor.textFileService.isDirty(toResource.call(this, '/path/index_async2.txt'))); assert.ok(!accessor.textFileService.isDirty(toResource.call(this, '/path/index_async.txt'))); @@ -372,7 +393,8 @@ suite('Files - TextFileEditorModel', () => { assert.ok(accessor.textFileService.isDirty(toResource.call(this, '/path/index_async.txt'))); await timeout(10); - await accessor.textFileService.saveAll(); + await accessor.textFileService.save(toResource.call(this, '/path/index_async.txt')); + await accessor.textFileService.save(toResource.call(this, '/path/index_async2.txt')); assert.ok(!accessor.textFileService.isDirty(toResource.call(this, '/path/index_async.txt'))); assert.ok(!accessor.textFileService.isDirty(toResource.call(this, '/path/index_async2.txt'))); assert.ok(assertIsDefined(model1.getStat()).mtime > m1Mtime); @@ -388,12 +410,10 @@ suite('Files - TextFileEditorModel', () => { let eventCounter = 0; const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); - model.onDidStateChange(e => { - if (e === StateChange.SAVED) { - assert.equal(snapshotToString(model.createSnapshot()!), 'bar'); - assert.ok(!model.isDirty()); - eventCounter++; - } + model.onDidSave(e => { + assert.equal(snapshotToString(model.createSnapshot()!), 'bar'); + assert.ok(!model.isDirty()); + eventCounter++; }); TextFileEditorModel.setSaveParticipant({ @@ -447,83 +467,4 @@ suite('Files - TextFileEditorModel', () => { await model.save(); model.dispose(); }); - - test('SaveSequentializer - pending basics', async function () { - const sequentializer = new SaveSequentializer(); - - assert.ok(!sequentializer.hasPendingSave()); - assert.ok(!sequentializer.hasPendingSave(2323)); - assert.ok(!sequentializer.pendingSave); - - // pending removes itself after done - await sequentializer.setPending(1, Promise.resolve()); - assert.ok(!sequentializer.hasPendingSave()); - assert.ok(!sequentializer.hasPendingSave(1)); - assert.ok(!sequentializer.pendingSave); - - // pending removes itself after done (use timeout) - sequentializer.setPending(2, timeout(1)); - assert.ok(sequentializer.hasPendingSave()); - assert.ok(sequentializer.hasPendingSave(2)); - assert.ok(!sequentializer.hasPendingSave(1)); - assert.ok(sequentializer.pendingSave); - - await timeout(2); - assert.ok(!sequentializer.hasPendingSave()); - assert.ok(!sequentializer.hasPendingSave(2)); - assert.ok(!sequentializer.pendingSave); - }); - - test('SaveSequentializer - pending and next (finishes instantly)', async function () { - const sequentializer = new SaveSequentializer(); - - let pendingDone = false; - sequentializer.setPending(1, timeout(1).then(() => { pendingDone = true; return; })); - - // next finishes instantly - let nextDone = false; - const res = sequentializer.setNext(() => Promise.resolve(null).then(() => { nextDone = true; return; })); - - await res; - assert.ok(pendingDone); - assert.ok(nextDone); - }); - - test('SaveSequentializer - pending and next (finishes after timeout)', async function () { - const sequentializer = new SaveSequentializer(); - - let pendingDone = false; - sequentializer.setPending(1, timeout(1).then(() => { pendingDone = true; return; })); - - // next finishes after timeout - let nextDone = false; - const res = sequentializer.setNext(() => timeout(1).then(() => { nextDone = true; return; })); - - await res; - assert.ok(pendingDone); - assert.ok(nextDone); - }); - - test('SaveSequentializer - pending and multiple next (last one wins)', async function () { - const sequentializer = new SaveSequentializer(); - - let pendingDone = false; - sequentializer.setPending(1, timeout(1).then(() => { pendingDone = true; return; })); - - // next finishes after timeout - let firstDone = false; - let firstRes = sequentializer.setNext(() => timeout(2).then(() => { firstDone = true; return; })); - - let secondDone = false; - let secondRes = sequentializer.setNext(() => timeout(3).then(() => { secondDone = true; return; })); - - let thirdDone = false; - let thirdRes = sequentializer.setNext(() => timeout(4).then(() => { thirdDone = true; return; })); - - await Promise.all([firstRes, secondRes, thirdRes]); - assert.ok(pendingDone); - assert.ok(!firstDone); - assert.ok(!secondDone); - assert.ok(thirdDone); - }); }); diff --git a/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts b/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts index db3a03da9b..644e684983 100644 --- a/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts +++ b/src/vs/workbench/services/textfile/test/textFileEditorModelManager.test.ts @@ -11,17 +11,9 @@ import { workbenchInstantiationService, TestFileService } from 'vs/workbench/tes 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 { timeout } from 'vs/base/common/async'; import { toResource } from 'vs/base/test/common/utils'; import { ModesRegistry, PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; -export class TestTextFileEditorModelManager extends TextFileEditorModelManager { - - protected debounceDelay(): number { - return 10; - } -} - class ServiceAccessor { constructor( @IFileService public fileService: TestFileService, @@ -41,7 +33,7 @@ suite('Files - TextFileEditorModelManager', () => { }); test('add, remove, clear, get, getAll', function () { - const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager); + const manager: TextFileEditorModelManager = instantiationService.createInstance(TextFileEditorModelManager); const model1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random1.txt'), 'utf8', undefined); const model2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random2.txt'), 'utf8', undefined); @@ -95,28 +87,28 @@ suite('Files - TextFileEditorModelManager', () => { model3.dispose(); }); - test('loadOrCreate', async () => { - const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager); + test('resolve', async () => { + const manager: TextFileEditorModelManager = instantiationService.createInstance(TextFileEditorModelManager); const resource = URI.file('/test.html'); const encoding = 'utf8'; - const model = await manager.loadOrCreate(resource, { encoding }); + const model = await manager.resolve(resource, { encoding }); assert.ok(model); assert.equal(model.getEncoding(), encoding); assert.equal(manager.get(resource), model); - const model2 = await manager.loadOrCreate(resource, { encoding }); + const model2 = await manager.resolve(resource, { encoding }); assert.equal(model2, model); model.dispose(); - const model3 = await manager.loadOrCreate(resource, { encoding }); + const model3 = await manager.resolve(resource, { encoding }); assert.notEqual(model3, model2); assert.equal(manager.get(resource), model3); model3.dispose(); }); test('removed from cache when model disposed', function () { - const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager); + const manager: TextFileEditorModelManager = instantiationService.createInstance(TextFileEditorModelManager); const model1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random1.txt'), 'utf8', undefined); const model2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/random2.txt'), 'utf8', undefined); @@ -136,60 +128,61 @@ suite('Files - TextFileEditorModelManager', () => { }); test('events', async function () { - TextFileEditorModel.DEFAULT_CONTENT_CHANGE_BUFFER_DELAY = 0; - TextFileEditorModel.DEFAULT_ORPHANED_CHANGE_BUFFER_DELAY = 0; - - const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager); + const manager: TextFileEditorModelManager = instantiationService.createInstance(TextFileEditorModelManager); const resource1 = toResource.call(this, '/path/index.txt'); const resource2 = toResource.call(this, '/path/other.txt'); - let dirtyCounter = 0; + let loadedCounter = 0; + let gotDirtyCounter = 0; + let gotNonDirtyCounter = 0; let revertedCounter = 0; let savedCounter = 0; let encodingCounter = 0; - let disposeCounter = 0; - let contentCounter = 0; - manager.onModelDirty(e => { - if (e.resource.toString() === resource1.toString()) { - dirtyCounter++; + manager.onDidLoad(({ model }) => { + if (model.resource.toString() === resource1.toString()) { + loadedCounter++; } }); - manager.onModelReverted(e => { - if (e.resource.toString() === resource1.toString()) { + manager.onDidChangeDirty(model => { + if (model.resource.toString() === resource1.toString()) { + if (model.isDirty()) { + gotDirtyCounter++; + } else { + gotNonDirtyCounter++; + } + } + }); + + manager.onDidRevert(model => { + if (model.resource.toString() === resource1.toString()) { revertedCounter++; } }); - manager.onModelSaved(e => { - if (e.resource.toString() === resource1.toString()) { + manager.onDidSave(({ model }) => { + if (model.resource.toString() === resource1.toString()) { savedCounter++; } }); - manager.onModelEncodingChanged(e => { - if (e.resource.toString() === resource1.toString()) { + manager.onDidChangeEncoding(model => { + if (model.resource.toString() === resource1.toString()) { encodingCounter++; } }); - manager.onModelContentChanged(e => { - if (e.resource.toString() === resource1.toString()) { - contentCounter++; - } - }); + const model1 = await manager.resolve(resource1, { encoding: 'utf8' }); + assert.equal(loadedCounter, 1); - manager.onModelDisposed(e => { - disposeCounter++; - }); - - const model1 = await manager.loadOrCreate(resource1, { encoding: 'utf8' }); accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource: resource1, type: FileChangeType.DELETED }])); accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource: resource1, type: FileChangeType.ADDED }])); - const model2 = await manager.loadOrCreate(resource2, { encoding: 'utf8' }); + const model2 = await manager.resolve(resource2, { encoding: 'utf8' }); + assert.equal(loadedCounter, 2); + model1.textEditorModel!.setValue('changed'); model1.updatePreferredEncoding('utf16'); @@ -199,66 +192,14 @@ suite('Files - TextFileEditorModelManager', () => { await model1.save(); model1.dispose(); model2.dispose(); - assert.equal(disposeCounter, 2); await model1.revert(); - assert.equal(dirtyCounter, 2); + assert.equal(gotDirtyCounter, 2); + assert.equal(gotNonDirtyCounter, 2); assert.equal(revertedCounter, 1); assert.equal(savedCounter, 1); assert.equal(encodingCounter, 2); - await timeout(10); - assert.equal(contentCounter, 2); - model1.dispose(); - model2.dispose(); - assert.ok(!accessor.modelService.getModel(resource1)); - assert.ok(!accessor.modelService.getModel(resource2)); - }); - - test('events debounced', async function () { - const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager); - - const resource1 = toResource.call(this, '/path/index.txt'); - const resource2 = toResource.call(this, '/path/other.txt'); - - let dirtyCounter = 0; - let revertedCounter = 0; - let savedCounter = 0; - - TextFileEditorModel.DEFAULT_CONTENT_CHANGE_BUFFER_DELAY = 0; - - manager.onModelsDirty(e => { - dirtyCounter += e.length; - assert.equal(e[0].resource.toString(), resource1.toString()); - }); - - manager.onModelsReverted(e => { - revertedCounter += e.length; - assert.equal(e[0].resource.toString(), resource1.toString()); - }); - - manager.onModelsSaved(e => { - savedCounter += e.length; - assert.equal(e[0].resource.toString(), resource1.toString()); - }); - - const model1 = await manager.loadOrCreate(resource1, { encoding: 'utf8' }); - const model2 = await manager.loadOrCreate(resource2, { encoding: 'utf8' }); - model1.textEditorModel!.setValue('changed'); - model1.updatePreferredEncoding('utf16'); - - await model1.revert(); - model1.textEditorModel!.setValue('changed again'); - - await model1.save(); - model1.dispose(); - model2.dispose(); - - await model1.revert(); - await timeout(20); - assert.equal(dirtyCounter, 2); - assert.equal(revertedCounter, 1); - assert.equal(savedCounter, 1); model1.dispose(); model2.dispose(); assert.ok(!accessor.modelService.getModel(resource1)); @@ -266,11 +207,11 @@ suite('Files - TextFileEditorModelManager', () => { }); test('disposing model takes it out of the manager', async function () { - const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager); + const manager: TextFileEditorModelManager = instantiationService.createInstance(TextFileEditorModelManager); const resource = toResource.call(this, '/path/index_something.txt'); - const model = await manager.loadOrCreate(resource, { encoding: 'utf8' }); + const model = await manager.resolve(resource, { encoding: 'utf8' }); model.dispose(); assert.ok(!manager.get(resource)); assert.ok(!accessor.modelService.getModel(model.resource)); @@ -278,11 +219,11 @@ suite('Files - TextFileEditorModelManager', () => { }); test('dispose prevents dirty model from getting disposed', async function () { - const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager); + const manager: TextFileEditorModelManager = instantiationService.createInstance(TextFileEditorModelManager); const resource = toResource.call(this, '/path/index_something.txt'); - const model = await manager.loadOrCreate(resource, { encoding: 'utf8' }); + const model = await manager.resolve(resource, { encoding: 'utf8' }); model.textEditorModel!.setValue('make dirty'); manager.disposeModel((model as TextFileEditorModel)); assert.ok(!model.isDisposed()); @@ -298,14 +239,14 @@ suite('Files - TextFileEditorModelManager', () => { id: mode, }); - const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager); + const manager: TextFileEditorModelManager = instantiationService.createInstance(TextFileEditorModelManager); const resource = toResource.call(this, '/path/index_something.txt'); - let model = await manager.loadOrCreate(resource, { mode }); + let model = await manager.resolve(resource, { mode }); assert.equal(model.textEditorModel!.getModeId(), mode); - model = await manager.loadOrCreate(resource, { mode: 'text' }); + model = await manager.resolve(resource, { mode: 'text' }); assert.equal(model.textEditorModel!.getModeId(), PLAINTEXT_MODE_ID); manager.disposeModel((model as TextFileEditorModel)); diff --git a/src/vs/workbench/services/textfile/test/textFileService.io.test.ts b/src/vs/workbench/services/textfile/test/textFileService.io.test.ts index dc748f0001..6a1eb7756c 100644 --- a/src/vs/workbench/services/textfile/test/textFileService.io.test.ts +++ b/src/vs/workbench/services/textfile/test/textFileService.io.test.ts @@ -6,7 +6,6 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { workbenchInstantiationService, TestTextFileService } from 'vs/workbench/test/workbenchTestServices'; import { ITextFileService, snapshotToString, TextFileOperationResult, TextFileOperationError } from 'vs/workbench/services/textfile/common/textfiles'; -import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { IFileService } from 'vs/platform/files/common/files'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { Schemas } from 'vs/base/common/network'; @@ -31,8 +30,7 @@ import { detectEncodingByBOM } from 'vs/base/test/node/encoding/encoding.test'; class ServiceAccessor { constructor( - @ITextFileService public textFileService: TestTextFileService, - @IUntitledTextEditorService public untitledTextEditorService: IUntitledTextEditorService + @ITextFileService public textFileService: TestTextFileService ) { } } @@ -94,9 +92,7 @@ suite('Files - TextFileService i/o', () => { }); teardown(async () => { - (accessor.textFileService.models).clear(); - (accessor.textFileService.models).dispose(); - accessor.untitledTextEditorService.revertAll(); + (accessor.textFileService.files).dispose(); disposables.clear(); diff --git a/src/vs/workbench/services/textfile/test/textFileService.test.ts b/src/vs/workbench/services/textfile/test/textFileService.test.ts index 4bd0c15a52..c73ea523e9 100644 --- a/src/vs/workbench/services/textfile/test/textFileService.test.ts +++ b/src/vs/workbench/services/textfile/test/textFileService.test.ts @@ -3,32 +3,27 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import * as sinon from 'sinon'; -import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; -import { ILifecycleService, BeforeShutdownEvent, ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle'; +import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { workbenchInstantiationService, TestLifecycleService, TestTextFileService, TestContextService, TestFileService, TestElectronService, TestFilesConfigurationService, TestFileDialogService } from 'vs/workbench/test/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 { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; -import { HotExitConfiguration, IFileService } from 'vs/platform/files/common/files'; +import { IFileService } from 'vs/platform/files/common/files'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; -import { IWorkspaceContextService, Workspace } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; -import { Schemas } from 'vs/base/common/network'; import { IElectronService } from 'vs/platform/electron/node/electron'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; -import { IFileDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; +import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; class ServiceAccessor { constructor( @ILifecycleService public lifecycleService: TestLifecycleService, @ITextFileService public textFileService: TestTextFileService, @IFilesConfigurationService public filesConfigurationService: TestFilesConfigurationService, - @IUntitledTextEditorService public untitledTextEditorService: IUntitledTextEditorService, @IWorkspaceContextService public contextService: TestContextService, @IModelService public modelService: ModelServiceImpl, @IFileService public fileService: TestFileService, @@ -38,16 +33,6 @@ class ServiceAccessor { } } -class BeforeShutdownEventImpl implements BeforeShutdownEvent { - - value: boolean | Promise | undefined; - reason = ShutdownReason.CLOSE; - - veto(value: boolean | Promise): void { - this.value = value; - } -} - suite('Files - TextFileService', () => { let instantiationService: IInstantiationService; @@ -63,215 +48,94 @@ suite('Files - TextFileService', () => { if (model) { model.dispose(); } - (accessor.textFileService.models).clear(); - (accessor.textFileService.models).dispose(); - accessor.untitledTextEditorService.revertAll(); - }); - - test('confirm onWillShutdown - no veto', async function () { - model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); - (accessor.textFileService.models).add(model.resource, model); - - const event = new BeforeShutdownEventImpl(); - accessor.lifecycleService.fireWillShutdown(event); - - const veto = event.value; - if (typeof veto === 'boolean') { - assert.ok(!veto); - } else { - assert.ok(!(await veto)); - } - }); - - test('confirm onWillShutdown - veto if user cancels', async function () { - model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); - (accessor.textFileService.models).add(model.resource, model); - - const service = accessor.textFileService; - accessor.fileDialogService.setConfirmResult(ConfirmResult.CANCEL); - - await model.load(); - model.textEditorModel!.setValue('foo'); - assert.equal(service.getDirty().length, 1); - - const event = new BeforeShutdownEventImpl(); - accessor.lifecycleService.fireWillShutdown(event); - assert.ok(event.value); - }); - - test('confirm onWillShutdown - no veto and backups cleaned up if user does not want to save (hot.exit: off)', async function () { - model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); - (accessor.textFileService.models).add(model.resource, model); - - const service = accessor.textFileService; - accessor.fileDialogService.setConfirmResult(ConfirmResult.DONT_SAVE); - accessor.filesConfigurationService.onFilesConfigurationChange({ files: { hotExit: 'off' } }); - - await model.load(); - model.textEditorModel!.setValue('foo'); - assert.equal(service.getDirty().length, 1); - const event = new BeforeShutdownEventImpl(); - accessor.lifecycleService.fireWillShutdown(event); - - let veto = event.value; - if (typeof veto === 'boolean') { - assert.ok(service.cleanupBackupsBeforeShutdownCalled); - assert.ok(!veto); - return; - } - - veto = await veto; - assert.ok(service.cleanupBackupsBeforeShutdownCalled); - assert.ok(!veto); - }); - - test('confirm onWillShutdown - save (hot.exit: off)', async function () { - model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); - (accessor.textFileService.models).add(model.resource, model); - - const service = accessor.textFileService; - accessor.fileDialogService.setConfirmResult(ConfirmResult.SAVE); - accessor.filesConfigurationService.onFilesConfigurationChange({ files: { hotExit: 'off' } }); - - await model.load(); - model.textEditorModel!.setValue('foo'); - assert.equal(service.getDirty().length, 1); - const event = new BeforeShutdownEventImpl(); - accessor.lifecycleService.fireWillShutdown(event); - - const veto = await (>event.value); - assert.ok(!veto); - assert.ok(!model.isDirty()); + (accessor.textFileService.files).dispose(); }); test('isDirty/getDirty - files and untitled', async function () { model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); - (accessor.textFileService.models).add(model.resource, model); - - const service = accessor.textFileService; + (accessor.textFileService.files).add(model.resource, model); await model.load(); - assert.ok(!service.isDirty(model.resource)); + assert.ok(!accessor.textFileService.isDirty(model.resource)); model.textEditorModel!.setValue('foo'); - assert.ok(service.isDirty(model.resource)); - assert.equal(service.getDirty().length, 1); - assert.equal(service.getDirty([model.resource])[0].toString(), model.resource.toString()); + assert.ok(accessor.textFileService.isDirty(model.resource)); - const untitled = accessor.untitledTextEditorService.createOrGet(); - const untitledModel = await untitled.resolve(); + const untitled = await accessor.textFileService.untitled.resolve(); - assert.ok(!service.isDirty(untitled.getResource())); - assert.equal(service.getDirty().length, 1); - untitledModel.textEditorModel!.setValue('changed'); + assert.ok(!accessor.textFileService.isDirty(untitled.resource)); + untitled.textEditorModel.setValue('changed'); - assert.ok(service.isDirty(untitled.getResource())); - assert.equal(service.getDirty().length, 2); - assert.equal(service.getDirty([untitled.getResource()])[0].toString(), untitled.getResource().toString()); + assert.ok(accessor.textFileService.isDirty(untitled.resource)); + + untitled.dispose(); }); test('save - file', async function () { model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); - (accessor.textFileService.models).add(model.resource, model); - - const service = accessor.textFileService; + (accessor.textFileService.files).add(model.resource, model); await model.load(); model.textEditorModel!.setValue('foo'); - assert.ok(service.isDirty(model.resource)); + assert.ok(accessor.textFileService.isDirty(model.resource)); - const res = await service.save(model.resource); + const res = await accessor.textFileService.save(model.resource); assert.ok(res); - assert.ok(!service.isDirty(model.resource)); - }); - - test('save - UNC path', async function () { - const untitledUncUri = URI.from({ scheme: 'untitled', authority: 'server', path: '/share/path/file.txt' }); - model = instantiationService.createInstance(TextFileEditorModel, untitledUncUri, 'utf8', undefined); - (accessor.textFileService.models).add(model.resource, model); - - const mockedFileUri = untitledUncUri.with({ scheme: Schemas.file }); - const mockedEditorInput = instantiationService.createInstance(TextFileEditorModel, mockedFileUri, 'utf8', undefined); - const loadOrCreateStub = sinon.stub(accessor.textFileService.models, 'loadOrCreate', () => Promise.resolve(mockedEditorInput)); - - sinon.stub(accessor.untitledTextEditorService, 'exists', () => true); - sinon.stub(accessor.untitledTextEditorService, 'hasAssociatedFilePath', () => true); - sinon.stub(accessor.modelService, 'updateModel', () => { }); - - await model.load(); - model.textEditorModel!.setValue('foo'); - - const res = await accessor.textFileService.saveAll(true); - assert.ok(loadOrCreateStub.calledOnce); - assert.equal(res.results.length, 1); - assert.ok(!res.results[0].error); - assert.equal(res.results[0].target!.scheme, Schemas.file); - assert.equal(res.results[0].target!.authority, untitledUncUri.authority); - assert.equal(res.results[0].target!.path, untitledUncUri.path); + assert.ok(!accessor.textFileService.isDirty(model.resource)); }); test('saveAll - file', async function () { model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); - (accessor.textFileService.models).add(model.resource, model); - - const service = accessor.textFileService; + (accessor.textFileService.files).add(model.resource, model); await model.load(); model.textEditorModel!.setValue('foo'); - assert.ok(service.isDirty(model.resource)); + assert.ok(accessor.textFileService.isDirty(model.resource)); - const res = await service.saveAll([model.resource]); + const res = await accessor.textFileService.save(model.resource); assert.ok(res); - assert.ok(!service.isDirty(model.resource)); - assert.equal(res.results.length, 1); - assert.equal(res.results[0].source.toString(), model.resource.toString()); + assert.ok(!accessor.textFileService.isDirty(model.resource)); }); test('saveAs - file', async function () { model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); - (accessor.textFileService.models).add(model.resource, model); - - const service = accessor.textFileService; - service.setPromptPath(model.resource); + (accessor.textFileService.files).add(model.resource, model); + accessor.textFileService.setPromptPath(model.resource); await model.load(); model.textEditorModel!.setValue('foo'); - assert.ok(service.isDirty(model.resource)); + assert.ok(accessor.textFileService.isDirty(model.resource)); - const res = await service.saveAs(model.resource); + const res = await accessor.textFileService.saveAs(model.resource); assert.equal(res!.toString(), model.resource.toString()); - assert.ok(!service.isDirty(model.resource)); + assert.ok(!accessor.textFileService.isDirty(model.resource)); }); test('revert - file', async function () { model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); - (accessor.textFileService.models).add(model.resource, model); - - const service = accessor.textFileService; - service.setPromptPath(model.resource); + (accessor.textFileService.files).add(model.resource, model); + accessor.textFileService.setPromptPath(model.resource); await model.load(); model!.textEditorModel!.setValue('foo'); - assert.ok(service.isDirty(model.resource)); + assert.ok(accessor.textFileService.isDirty(model.resource)); - const res = await service.revert(model.resource); + const res = await accessor.textFileService.revert(model.resource); assert.ok(res); - assert.ok(!service.isDirty(model.resource)); + assert.ok(!accessor.textFileService.isDirty(model.resource)); }); test('delete - dirty file', async function () { model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); - (accessor.textFileService.models).add(model.resource, model); - - const service = accessor.textFileService; + (accessor.textFileService.files).add(model.resource, model); await model.load(); model!.textEditorModel!.setValue('foo'); - assert.ok(service.isDirty(model.resource)); + assert.ok(accessor.textFileService.isDirty(model.resource)); - await service.delete(model.resource); - assert.ok(!service.isDirty(model.resource)); + await accessor.textFileService.delete(model.resource); + assert.ok(!accessor.textFileService.isDirty(model.resource)); }); test('move - dirty file', async function () { @@ -285,163 +149,27 @@ suite('Files - TextFileService', () => { async function testMove(source: URI, target: URI, targetDirty?: boolean): Promise { let sourceModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, source, 'utf8', undefined); let targetModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, target, 'utf8', undefined); - (accessor.textFileService.models).add(sourceModel.resource, sourceModel); - (accessor.textFileService.models).add(targetModel.resource, targetModel); - - const service = accessor.textFileService; + (accessor.textFileService.files).add(sourceModel.resource, sourceModel); + (accessor.textFileService.files).add(targetModel.resource, targetModel); await sourceModel.load(); sourceModel.textEditorModel!.setValue('foo'); - assert.ok(service.isDirty(sourceModel.resource)); + assert.ok(accessor.textFileService.isDirty(sourceModel.resource)); if (targetDirty) { await targetModel.load(); targetModel.textEditorModel!.setValue('bar'); - assert.ok(service.isDirty(targetModel.resource)); + assert.ok(accessor.textFileService.isDirty(targetModel.resource)); } - await service.move(sourceModel.resource, targetModel.resource, true); + await accessor.textFileService.move(sourceModel.resource, targetModel.resource, true); assert.equal(targetModel.textEditorModel!.getValue(), 'foo'); - assert.ok(!service.isDirty(sourceModel.resource)); - assert.ok(service.isDirty(targetModel.resource)); + assert.ok(!accessor.textFileService.isDirty(sourceModel.resource)); + assert.ok(accessor.textFileService.isDirty(targetModel.resource)); sourceModel.dispose(); targetModel.dispose(); } - - suite.skip('Hot Exit', () => { // {{SQL CARBON EDIT}} skip suite - suite('"onExit" setting', () => { - test('should hot exit on non-Mac (reason: CLOSE, windows: single, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.CLOSE, false, true, !!platform.isMacintosh); - }); - test('should hot exit on non-Mac (reason: CLOSE, windows: single, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.CLOSE, false, false, !!platform.isMacintosh); - }); - test('should NOT hot exit (reason: CLOSE, windows: multiple, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.CLOSE, true, true, true); - }); - test('should NOT hot exit (reason: CLOSE, windows: multiple, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.CLOSE, true, false, true); - }); - test('should hot exit (reason: QUIT, windows: single, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.QUIT, false, true, false); - }); - test('should hot exit (reason: QUIT, windows: single, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.QUIT, false, false, false); - }); - test('should hot exit (reason: QUIT, windows: multiple, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.QUIT, true, true, false); - }); - test('should hot exit (reason: QUIT, windows: multiple, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.QUIT, true, false, false); - }); - test('should hot exit (reason: RELOAD, windows: single, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.RELOAD, false, true, false); - }); - test('should hot exit (reason: RELOAD, windows: single, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.RELOAD, false, false, false); - }); - test('should hot exit (reason: RELOAD, windows: multiple, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.RELOAD, true, true, false); - }); - test('should hot exit (reason: RELOAD, windows: multiple, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.RELOAD, true, false, false); - }); - test('should NOT hot exit (reason: LOAD, windows: single, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.LOAD, false, true, true); - }); - test('should NOT hot exit (reason: LOAD, windows: single, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.LOAD, false, false, true); - }); - test('should NOT hot exit (reason: LOAD, windows: multiple, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.LOAD, true, true, true); - }); - test('should NOT hot exit (reason: LOAD, windows: multiple, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT, ShutdownReason.LOAD, true, false, true); - }); - }); - - suite('"onExitAndWindowClose" setting', () => { - test('should hot exit (reason: CLOSE, windows: single, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.CLOSE, false, true, false); - }); - test('should hot exit (reason: CLOSE, windows: single, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.CLOSE, false, false, !!platform.isMacintosh); - }); - test('should hot exit (reason: CLOSE, windows: multiple, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.CLOSE, true, true, false); - }); - test('should NOT hot exit (reason: CLOSE, windows: multiple, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.CLOSE, true, false, true); - }); - test('should hot exit (reason: QUIT, windows: single, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.QUIT, false, true, false); - }); - test('should hot exit (reason: QUIT, windows: single, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.QUIT, false, false, false); - }); - test('should hot exit (reason: QUIT, windows: multiple, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.QUIT, true, true, false); - }); - test('should hot exit (reason: QUIT, windows: multiple, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.QUIT, true, false, false); - }); - test('should hot exit (reason: RELOAD, windows: single, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.RELOAD, false, true, false); - }); - test('should hot exit (reason: RELOAD, windows: single, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.RELOAD, false, false, false); - }); - test('should hot exit (reason: RELOAD, windows: multiple, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.RELOAD, true, true, false); - }); - test('should hot exit (reason: RELOAD, windows: multiple, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.RELOAD, true, false, false); - }); - test('should hot exit (reason: LOAD, windows: single, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.LOAD, false, true, false); - }); - test('should NOT hot exit (reason: LOAD, windows: single, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.LOAD, false, false, true); - }); - test('should hot exit (reason: LOAD, windows: multiple, workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.LOAD, true, true, false); - }); - test('should NOT hot exit (reason: LOAD, windows: multiple, empty workspace)', function () { - return hotExitTest.call(this, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE, ShutdownReason.LOAD, true, false, true); - }); - }); - - async function hotExitTest(this: any, setting: string, shutdownReason: ShutdownReason, multipleWindows: boolean, workspace: boolean, shouldVeto: boolean): Promise { - model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined); - (accessor.textFileService.models).add(model.resource, model); - - const service = accessor.textFileService; - // Set hot exit config - accessor.filesConfigurationService.onFilesConfigurationChange({ files: { hotExit: setting } }); - // Set empty workspace if required - if (!workspace) { - accessor.contextService.setWorkspace(new Workspace('empty:1508317022751')); - } - // Set multiple windows if required - if (multipleWindows) { - accessor.electronService.windowCount = Promise.resolve(2); - } - // Set cancel to force a veto if hot exit does not trigger - accessor.fileDialogService.setConfirmResult(ConfirmResult.CANCEL); - - await model.load(); - model.textEditorModel!.setValue('foo'); - assert.equal(service.getDirty().length, 1); - const event = new BeforeShutdownEventImpl(); - event.reason = shutdownReason; - accessor.lifecycleService.fireWillShutdown(event); - - const veto = await (>event.value); - assert.ok(!service.cleanupBackupsBeforeShutdownCalled); // When hot exit is set, backups should never be cleaned since the confirm result is cancel - assert.equal(veto, shouldVeto); - } - }); }); diff --git a/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts b/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts index 58906fdc61..c72f58ad65 100644 --- a/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts +++ b/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts @@ -38,7 +38,7 @@ class ResourceModelCollection extends ReferenceCollection { if (this.modelsToDispose.has(key)) { if (model instanceof TextFileEditorModel) { - this.textFileService.models.disposeModel(model); + this.textFileService.files.disposeModel(model); } else { model.dispose(); } @@ -141,9 +141,9 @@ export class TextModelResolverService implements ITextModelService { // Untitled Schema: go through cached input if (resource.scheme === network.Schemas.untitled) { - const model = await this.untitledTextEditorService.loadOrCreate({ resource }); + const model = await this.untitledTextEditorService.resolve({ untitledResource: resource }); - return new ImmortalReference(model as IResolvedTextEditorModel); + return new ImmortalReference(model); } // InMemory Schema: go through model service cache @@ -175,6 +175,10 @@ export class TextModelResolverService implements ITextModelService { } hasTextModelContentProvider(scheme: string): boolean { + if (scheme === network.Schemas.untitled || scheme === network.Schemas.inMemory) { + return true; // we handle untitled:// and inMemory:// within + } + return this.resourceModelCollection.hasTextModelContentProvider(scheme); } } diff --git a/src/vs/workbench/services/textmodelResolver/test/textModelResolverService.test.ts b/src/vs/workbench/services/textmodelResolver/test/textModelResolverService.test.ts index 42a8c582df..e41e6ffe90 100644 --- a/src/vs/workbench/services/textmodelResolver/test/textModelResolverService.test.ts +++ b/src/vs/workbench/services/textmodelResolver/test/textModelResolverService.test.ts @@ -48,9 +48,7 @@ suite('Workbench - TextModelResolverService', () => { model.dispose(); model = (undefined)!; } - (accessor.textFileService.models).clear(); - (accessor.textFileService.models).dispose(); - accessor.untitledTextEditorService.revertAll(); + (accessor.textFileService.files).dispose(); }); test('resolve resource', async () => { @@ -88,7 +86,7 @@ suite('Workbench - TextModelResolverService', () => { test('resolve file', async function () { const textModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_resolver.txt'), 'utf8', undefined); - (accessor.textFileService.models).add(textModel.resource, textModel); + (accessor.textFileService.files).add(textModel.resource, textModel); await textModel.load(); @@ -112,7 +110,7 @@ suite('Workbench - TextModelResolverService', () => { test('resolve untitled', async () => { const service = accessor.untitledTextEditorService; - const input = service.createOrGet(); + const input = service.create(); await input.resolve(); const ref = await accessor.textModelResolverService.createModelReference(input.getResource()); @@ -121,6 +119,7 @@ suite('Workbench - TextModelResolverService', () => { assert.ok(editorModel); ref.dispose(); input.dispose(); + model.dispose(); }); test('even loading documents should be refcounted', async () => { diff --git a/src/vs/workbench/services/textfile/common/textResourcePropertiesService.ts b/src/vs/workbench/services/textresourceProperties/common/textResourcePropertiesService.ts similarity index 100% rename from src/vs/workbench/services/textfile/common/textResourcePropertiesService.ts rename to src/vs/workbench/services/textresourceProperties/common/textResourcePropertiesService.ts diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index 2a93edd41d..56e7c8814b 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -40,7 +40,7 @@ import { IExtensionResourceLoaderService } from 'vs/workbench/services/extension const PREFERRED_DARK_THEME_SETTING = 'workbench.preferredDarkColorTheme'; const PREFERRED_LIGHT_THEME_SETTING = 'workbench.preferredLightColorTheme'; const PREFERRED_HC_THEME_SETTING = 'workbench.preferredHighContrastColorTheme'; -const DETECT_COLOR_SCHEME_SETTING = 'workbench.autoDetectColorScheme'; +const DETECT_COLOR_SCHEME_SETTING = 'window.autoDetectColorScheme'; const DETECT_HC_SETTING = 'window.autoDetectHighContrast'; // implementation @@ -194,6 +194,15 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { configurationRegistry.notifyConfigurationSchemaUpdated(themeSettingsConfiguration, tokenColorCustomizationConfiguration); + let colorThemeSetting = this.configurationService.getValue(COLOR_THEME_SETTING); + if (colorThemeSetting !== this.currentColorTheme.settingsId) { + const theme = await this.colorThemeStore.findThemeDataBySettingsId(colorThemeSetting, undefined); + if (theme) { + this.setColorTheme(theme.id, undefined); + return; + } + } + if (this.currentColorTheme.isLoaded) { const themeData = await this.colorThemeStore.findThemeData(this.currentColorTheme.id); if (!themeData) { @@ -218,6 +227,15 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { iconThemeSettingSchema.enumDescriptions = [iconThemeSettingSchema.enumDescriptions![0], ...event.themes.map(t => t.description || '')]; configurationRegistry.notifyConfigurationSchemaUpdated(themeSettingsConfiguration); + let iconThemeSetting = this.configurationService.getValue(ICON_THEME_SETTING); + if (iconThemeSetting !== this.currentIconTheme.settingsId) { + const theme = await this.iconThemeStore.findThemeBySettingsId(iconThemeSetting); + if (theme) { + this.setFileIconTheme(theme.id, undefined); + return; + } + } + if (this.currentIconTheme.isLoaded) { const theme = await this.iconThemeStore.findThemeData(this.currentIconTheme.id); if (!theme) { @@ -743,7 +761,7 @@ const preferredHCThemeSettingSchema: IConfigurationPropertySchema = { const detectColorSchemeSettingSchema: IConfigurationPropertySchema = { type: 'boolean', description: nls.localize('detectColorScheme', 'If set, automatically switch to the preferred color theme based on the OS appearance.'), - default: true + default: false }; const iconThemeSettingSchema: IConfigurationPropertySchema = { diff --git a/src/vs/workbench/services/themes/common/colorThemeData.ts b/src/vs/workbench/services/themes/common/colorThemeData.ts index 2c9c2bc943..2d168eb0dc 100644 --- a/src/vs/workbench/services/themes/common/colorThemeData.ts +++ b/src/vs/workbench/services/themes/common/colorThemeData.ts @@ -19,7 +19,7 @@ import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages'; import { URI } from 'vs/base/common/uri'; import { parse as parsePList } from 'vs/workbench/services/themes/common/plistParser'; import { startsWith } from 'vs/base/common/strings'; -import { TokenStyle, TokenClassification, ProbeScope, TokenStylingRule, getTokenClassificationRegistry, TokenStyleValue, matchTokenStylingRule } from 'vs/platform/theme/common/tokenClassificationRegistry'; +import { TokenStyle, TokenClassification, ProbeScope, TokenStylingRule, getTokenClassificationRegistry, TokenStyleValue } from 'vs/platform/theme/common/tokenClassificationRegistry'; import { MatcherWithPriority, Matcher, createMatchers } from 'vs/workbench/services/themes/common/textMateScopeMatcher'; import { IExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader'; import { FontStyle, ColorId, MetadataConsts } from 'vs/editor/common/modes'; @@ -153,7 +153,7 @@ export class ColorThemeData implements IColorTheme { } if (this.tokenStylingRules === undefined) { for (const rule of tokenClassificationRegistry.getTokenStylingDefaultRules()) { - const matchScore = matchTokenStylingRule(rule, classification); + const matchScore = rule.match(classification); if (matchScore >= 0) { let style: TokenStyle | undefined; if (rule.defaults.scopesToProbe) { @@ -169,14 +169,14 @@ export class ColorThemeData implements IColorTheme { } } else { for (const rule of this.tokenStylingRules) { - const matchScore = matchTokenStylingRule(rule, classification); + const matchScore = rule.match(classification); if (matchScore >= 0) { _processStyle(matchScore, rule.value); } } } for (const rule of this.customTokenStylingRules) { - const matchScore = matchTokenStylingRule(rule, classification); + const matchScore = rule.match(classification); if (matchScore >= 0) { _processStyle(matchScore, rule.value); } diff --git a/src/vs/workbench/services/themes/common/tokenClassificationExtensionPoint.ts b/src/vs/workbench/services/themes/common/tokenClassificationExtensionPoint.ts index 2c0d9a58f8..3f7711f616 100644 --- a/src/vs/workbench/services/themes/common/tokenClassificationExtensionPoint.ts +++ b/src/vs/workbench/services/themes/common/tokenClassificationExtensionPoint.ts @@ -228,7 +228,7 @@ export class TokenClassificationExtensionPoints { if (contribution.scopes) { if ((!Array.isArray(contribution.scopes) || contribution.scopes.some(s => typeof s !== 'string'))) { - collector.error(nls.localize('invalid.scopes', "If defined, 'configuration.tokenStyleDefaults.scopes' must must be an array or strings")); + collector.error(nls.localize('invalid.scopes', "If defined, 'configuration.tokenStyleDefaults.scopes' must be an array or strings")); continue; } tokenStyleDefault.scopesToProbe = [contribution.scopes]; diff --git a/src/vs/workbench/services/themes/test/electron-browser/tokenStyleResolving.test.ts b/src/vs/workbench/services/themes/test/electron-browser/tokenStyleResolving.test.ts index 7a80489781..0d9b909940 100644 --- a/src/vs/workbench/services/themes/test/electron-browser/tokenStyleResolving.test.ts +++ b/src/vs/workbench/services/themes/test/electron-browser/tokenStyleResolving.test.ts @@ -306,7 +306,7 @@ suite('Themes - TokenStyleResolving', () => { '*.static': { fontStyle: 'bold' }, '*.declaration': { fontStyle: 'italic' }, '*.async.static': { fontStyle: 'italic underline' }, - '*.async': { foreground: '#000fff', fontStyle: '-italic underline' } + '*.async': { foreground: '#000fff', fontStyle: 'underline' } }); assertTokenStyles(themeData, { @@ -316,7 +316,7 @@ suite('Themes - TokenStyleResolving', () => { 'class': ts('#0000ff', { italic: true }), 'class.static.declaration': ts('#0000ff', { bold: true, italic: true }), 'class.declaration': ts('#0000ff', { italic: true }), - 'class.declaration.async': ts('#000fff', { underline: true, italic: false }), + 'class.declaration.async': ts('#000fff', { underline: true, italic: true }), 'class.declaration.async.static': ts('#000fff', { italic: true, underline: true, bold: true }), }); diff --git a/src/vs/workbench/services/untitled/common/untitledTextEditorService.ts b/src/vs/workbench/services/untitled/common/untitledTextEditorService.ts index 3506631ecb..62d9e82d6f 100644 --- a/src/vs/workbench/services/untitled/common/untitledTextEditorService.ts +++ b/src/vs/workbench/services/untitled/common/untitledTextEditorService.ts @@ -5,41 +5,65 @@ import { URI } from 'vs/base/common/uri'; import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import * as arrays from 'vs/base/common/arrays'; import { UntitledTextEditorInput } from 'vs/workbench/common/editor/untitledTextEditorInput'; -import { IFilesConfiguration, IFileService } from 'vs/platform/files/common/files'; +import { IFilesConfiguration } from 'vs/platform/files/common/files'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Event, Emitter } from 'vs/base/common/event'; import { ResourceMap } from 'vs/base/common/map'; -import { UntitledTextEditorModel } from 'vs/workbench/common/editor/untitledTextEditorModel'; import { Schemas } from 'vs/base/common/network'; import { Disposable } from 'vs/base/common/lifecycle'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { basename } from 'vs/base/common/resources'; +import { UntitledTextEditorModel, IUntitledTextEditorModel } from 'vs/workbench/common/editor/untitledTextEditorModel'; +import { IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; export const IUntitledTextEditorService = createDecorator('untitledTextEditorService'); -export interface IModelLoadOrCreateOptions { - resource?: URI; - mode?: string; +export interface INewUntitledTextEditorOptions { + + /** + * Initial value of the untitled file. An untitled file with initial + * value is dirty right from the beginning. + */ initialValue?: string; + + /** + * Preferred language mode to use when saving the untitled file. + */ + mode?: string; + + /** + * Preferred encoding to use when saving the untitled file. + */ encoding?: string; - useResourcePath?: boolean; } -export interface IUntitledTextEditorService { - - _serviceBrand: undefined; +export interface IExistingUntitledTextEditorOptions extends INewUntitledTextEditorOptions { /** - * Events for when untitled text editors are created. + * A resource to identify the untitled resource to create or return + * if already existing. + * + * Note: the resource will not be used unless the scheme is `untitled`. */ - readonly onDidCreate: Event; + untitledResource?: URI; +} + +export interface INewUntitledTextEditorWithAssociatedResourceOptions extends INewUntitledTextEditorOptions { /** - * Events for when untitled text editors content changes (e.g. any keystroke). + * Resource components to associate with the untitled file. When saving + * the untitled file, the associated components will be used and the user + * is not being asked to provide a file path. + * + * Note: currently it is not possible to specify the `scheme` to use. The + * untitled file will saved to the default local or remote resource. */ - readonly onDidChangeContent: Event; + associatedResource?: { authority: string; path: string; query: string; fragment: string; } +} + +type IInternalUntitledTextEditorOptions = IExistingUntitledTextEditorOptions & INewUntitledTextEditorWithAssociatedResourceOptions; + +export interface IUntitledTextEditorModelManager { /** * Events for when untitled text editors change (e.g. getting dirty, saved or reverted). @@ -62,72 +86,43 @@ export interface IUntitledTextEditorService { exists(resource: URI): boolean; /** - * Returns dirty untitled text editors as resource URIs. + * Returns an existing untitled input if already created before. */ - getDirty(resources?: URI[]): URI[]; + get(resource: URI): UntitledTextEditorInput | undefined; /** - * Returns true if the provided resource is dirty. + * Creates a new untitled input with the provided options. If the `untitledResource` + * property is provided and the untitled input exists, it will return that existing + * instance instead of creating a new one. */ - isDirty(resource: URI): boolean; + create(options?: INewUntitledTextEditorOptions): UntitledTextEditorInput; + create(options?: INewUntitledTextEditorWithAssociatedResourceOptions): UntitledTextEditorInput; + create(options?: IExistingUntitledTextEditorOptions): UntitledTextEditorInput; /** - * Find out if a backup with the provided resource exists and has a backup on disk. + * Resolves an untitled editor model from the provided options. If the `untitledResource` + * property is provided and the untitled input exists, it will return that existing + * instance instead of creating a new one. */ - hasBackup(resource: URI): boolean; - - /** - * Reverts the untitled resources if found. - */ - revertAll(resources?: URI[]): URI[]; - - /** - * Creates a new untitled input with the optional resource URI or returns an existing one - * if the provided resource exists already as untitled input. - * - * It is valid to pass in a file resource. In that case the path will be used as identifier. - * The use case is to be able to create a new file with a specific path with VSCode. - */ - createOrGet(resource?: URI, mode?: string, initialValue?: string, encoding?: string): UntitledTextEditorInput; - - /** - * Creates a new untitled model with the optional resource URI or returns an existing one - * if the provided resource exists already as untitled model. - * - * It is valid to pass in a file resource. In that case the path will be used as identifier. - * The use case is to be able to create a new file with a specific path with VSCode. - */ - loadOrCreate(options: IModelLoadOrCreateOptions): Promise; + resolve(options?: INewUntitledTextEditorOptions): Promise; + resolve(options?: INewUntitledTextEditorWithAssociatedResourceOptions): Promise; + resolve(options?: IExistingUntitledTextEditorOptions): Promise; /** * A check to find out if a untitled resource has a file path associated or not. */ hasAssociatedFilePath(resource: URI): boolean; +} - /** - * Suggests a filename for the given untitled resource if it is known. - */ - suggestFileName(resource: URI): string; +export interface IUntitledTextEditorService extends IUntitledTextEditorModelManager { - /** - * Get the configured encoding for the given untitled resource if any. - */ - getEncoding(resource: URI): string | undefined; + _serviceBrand: undefined; } export class UntitledTextEditorService extends Disposable implements IUntitledTextEditorService { _serviceBrand: undefined; - private mapResourceToInput = new ResourceMap(); - private mapResourceToAssociatedFilePath = new ResourceMap(); - - private readonly _onDidCreate = this._register(new Emitter()); - readonly onDidCreate = this._onDidCreate.event; - - private readonly _onDidChangeContent = this._register(new Emitter()); - readonly onDidChangeContent = this._onDidChangeContent.event; - private readonly _onDidChangeDirty = this._register(new Emitter()); readonly onDidChangeDirty = this._onDidChangeDirty.event; @@ -137,132 +132,107 @@ export class UntitledTextEditorService extends Disposable implements IUntitledTe private readonly _onDidDisposeModel = this._register(new Emitter()); readonly onDidDisposeModel = this._onDidDisposeModel.event; + protected readonly mapResourceToInput = new ResourceMap(); + private readonly mapResourceToAssociatedFilePath = new ResourceMap(); + constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IFileService private readonly fileService: IFileService + @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); } - protected get(resource: URI): UntitledTextEditorInput | undefined { - return this.mapResourceToInput.get(resource); - } - - protected getAll(resources?: URI[]): UntitledTextEditorInput[] { - if (resources) { - return arrays.coalesce(resources.map(r => this.get(r))); - } - - return this.mapResourceToInput.values(); - } - exists(resource: URI): boolean { return this.mapResourceToInput.has(resource); } - revertAll(resources?: URI[], force?: boolean): URI[] { - const reverted: URI[] = []; - - const untitledInputs = this.getAll(resources); - untitledInputs.forEach(input => { - if (input) { - input.revert(); - - reverted.push(input.getResource()); - } - }); - - return reverted; + get(resource: URI): UntitledTextEditorInput | undefined { + return this.mapResourceToInput.get(resource); } - isDirty(resource: URI): boolean { - const input = this.get(resource); - - return input ? input.isDirty() : false; + resolve(options?: IInternalUntitledTextEditorOptions): Promise { + return this.doCreateOrGet(options).resolve(); } - hasBackup(resource: URI): boolean { - const input = this.get(resource); - - return input ? input.hasBackup() : false; + create(options?: IInternalUntitledTextEditorOptions): UntitledTextEditorInput { + return this.doCreateOrGet(options); } - getDirty(resources?: URI[]): URI[] { - let inputs: UntitledTextEditorInput[]; - if (resources) { - inputs = arrays.coalesce(resources.map(r => this.get(r))); - } else { - inputs = this.mapResourceToInput.values(); - } - - return inputs - .filter(i => i.isDirty()) - .map(i => i.getResource()); - } - - loadOrCreate(options: IModelLoadOrCreateOptions = Object.create(null)): Promise { - return this.createOrGet(options.resource, options.mode, options.initialValue, options.encoding, options.useResourcePath).resolve(); - } - - createOrGet(resource?: URI, mode?: string, initialValue?: string, encoding?: string, hasAssociatedFilePath: boolean = false): UntitledTextEditorInput { - if (resource) { - - // Massage resource if it comes with known file based resource - if (this.fileService.canHandleResource(resource)) { - hasAssociatedFilePath = true; - resource = resource.with({ scheme: Schemas.untitled }); // ensure we have the right scheme - } - - if (hasAssociatedFilePath) { - this.mapResourceToAssociatedFilePath.set(resource, true); // remember for future lookups - } - } + private doCreateOrGet(options: IInternalUntitledTextEditorOptions = Object.create(null)): UntitledTextEditorInput { + const massagedOptions = this.massageOptions(options); // Return existing instance if asked for it - if (resource && this.mapResourceToInput.has(resource)) { - return this.mapResourceToInput.get(resource)!; + if (massagedOptions.untitledResource && this.mapResourceToInput.has(massagedOptions.untitledResource)) { + return this.mapResourceToInput.get(massagedOptions.untitledResource)!; } - // Create new otherwise - return this.doCreate(resource, hasAssociatedFilePath, mode, initialValue, encoding); + // Create new instance otherwise + return this.doCreate(massagedOptions); } - private doCreate(resource?: URI, hasAssociatedFilePath?: boolean, mode?: string, initialValue?: string, encoding?: string): UntitledTextEditorInput { - let untitledResource: URI; - if (resource) { - untitledResource = resource; - } else { + private massageOptions(options: IInternalUntitledTextEditorOptions): IInternalUntitledTextEditorOptions { + const massagedOptions: IInternalUntitledTextEditorOptions = Object.create(null); - // Create new taking a resource URI that is not already taken - let counter = this.mapResourceToInput.size + 1; + // Figure out associated and untitled resource + if (options.associatedResource) { + massagedOptions.untitledResource = URI.from({ + scheme: Schemas.untitled, + authority: options.associatedResource.authority, + fragment: options.associatedResource.fragment, + path: options.associatedResource.path, + query: options.associatedResource.query + }); + massagedOptions.associatedResource = options.associatedResource; + } else { + if (options.untitledResource?.scheme === Schemas.untitled) { + massagedOptions.untitledResource = options.untitledResource; + } + } + + // Language mode + if (options.mode) { + massagedOptions.mode = options.mode; + } else if (!massagedOptions.associatedResource) { + const configuration = this.configurationService.getValue(); + if (configuration.files?.defaultLanguage) { + massagedOptions.mode = configuration.files.defaultLanguage; + } + } + + // Take over encoding and initial value + massagedOptions.encoding = options.encoding; + massagedOptions.initialValue = options.initialValue; + + return massagedOptions; + } + + private doCreate(options: IInternalUntitledTextEditorOptions): UntitledTextEditorInput { + + // Create a new untitled resource if none is provided + let untitledResource = options.untitledResource; + if (!untitledResource) { + let counter = 1; do { untitledResource = URI.from({ scheme: Schemas.untitled, path: `Untitled-${counter}` }); counter++; } while (this.mapResourceToInput.has(untitledResource)); } - // Look up default language from settings if any - if (!mode && !hasAssociatedFilePath) { - const configuration = this.configurationService.getValue(); - if (configuration.files?.defaultLanguage) { - mode = configuration.files.defaultLanguage; - } - } + // Create new input with provided options + const input = this.instantiationService.createInstance(UntitledTextEditorInput, untitledResource, !!options.associatedResource, options.mode, options.initialValue, options.encoding); - const input = this.instantiationService.createInstance(UntitledTextEditorInput, untitledResource, !!hasAssociatedFilePath, mode, initialValue, encoding); - - const contentListener = input.onDidModelChangeContent(() => this._onDidChangeContent.fire(untitledResource)); - const dirtyListener = input.onDidChangeDirty(() => this._onDidChangeDirty.fire(untitledResource)); - const encodingListener = input.onDidModelChangeEncoding(() => this._onDidChangeEncoding.fire(untitledResource)); - const disposeListener = input.onDispose(() => this._onDidDisposeModel.fire(untitledResource)); + const dirtyListener = input.onDidChangeDirty(() => this._onDidChangeDirty.fire(input.getResource())); + const encodingListener = input.onDidModelChangeEncoding(() => this._onDidChangeEncoding.fire(input.getResource())); + const disposeListener = input.onDispose(() => this._onDidDisposeModel.fire(input.getResource())); // Remove from cache on dispose - const onceDispose = Event.once(input.onDispose); - onceDispose(() => { + Event.once(input.onDispose)(() => { + + // Registry this.mapResourceToInput.delete(input.getResource()); this.mapResourceToAssociatedFilePath.delete(input.getResource()); - contentListener.dispose(); + + // Listeners dirtyListener.dispose(); encodingListener.dispose(); disposeListener.dispose(); @@ -270,9 +240,9 @@ export class UntitledTextEditorService extends Disposable implements IUntitledTe // Add to cache this.mapResourceToInput.set(untitledResource, input); - - // Signal new untitled as event - this._onDidCreate.fire(untitledResource); + if (options.associatedResource) { + this.mapResourceToAssociatedFilePath.set(untitledResource, true); + } return input; } @@ -280,18 +250,6 @@ export class UntitledTextEditorService extends Disposable implements IUntitledTe hasAssociatedFilePath(resource: URI): boolean { return this.mapResourceToAssociatedFilePath.has(resource); } - - suggestFileName(resource: URI): string { - const input = this.get(resource); - - return input ? input.suggestFileName() : basename(resource); - } - - getEncoding(resource: URI): string | undefined { - const input = this.get(resource); - - return input ? input.getEncoding() : undefined; - } } registerSingleton(IUntitledTextEditorService, UntitledTextEditorService, true); diff --git a/src/vs/workbench/services/userDataSync/common/userDataSyncUtil.ts b/src/vs/workbench/services/userDataSync/common/userDataSyncUtil.ts index d1c76f94a2..e3e7819c23 100644 --- a/src/vs/workbench/services/userDataSync/common/userDataSyncUtil.ts +++ b/src/vs/workbench/services/userDataSync/common/userDataSyncUtil.ts @@ -11,6 +11,14 @@ import { FormattingOptions } from 'vs/base/common/jsonFormatter'; import { URI } from 'vs/base/common/uri'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ITextResourcePropertiesService, ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; +import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import type { IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { localize } from 'vs/nls'; +import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; class UserDataSyncUtilService implements IUserDataSyncUtilService { @@ -21,8 +29,15 @@ class UserDataSyncUtilService implements IUserDataSyncUtilService { @ITextModelService private readonly textModelService: ITextModelService, @ITextResourcePropertiesService private readonly textResourcePropertiesService: ITextResourcePropertiesService, @ITextResourceConfigurationService private readonly textResourceConfigurationService: ITextResourceConfigurationService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IQuickInputService private readonly quickInputService: IQuickInputService, + @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService ) { } + public async updateConfigurationValue(key: string, value: any): Promise { + await this.configurationService.updateValue(key, value, ConfigurationTarget.USER); + } + public async resolveUserBindings(userBindings: string[]): Promise> { const keys: IStringDictionary = {}; for (const userbinding of userBindings) { @@ -46,6 +61,39 @@ class UserDataSyncUtilService implements IUserDataSyncUtilService { tabSize: this.textResourceConfigurationService.getValue(resource, 'editor.tabSize') }; } + + async ignoreExtensionsToSync(extensionIdentifiers: IExtensionIdentifier[]): Promise { + return new Promise(async (c, e) => { + const disposables: DisposableStore = new DisposableStore(); + const quickPick = this.quickInputService.createQuickPick<{ identifier: IExtensionIdentifier, label: string, description: string }>(); + disposables.add(quickPick); + quickPick.title = localize('select extensions', "Sync: Select Extensions to Sync"); + quickPick.placeholder = localize('choose extensions to sync', "Choose extensions to sync"); + quickPick.canSelectMany = true; + quickPick.ignoreFocusOut = true; + quickPick.busy = true; + quickPick.show(); + const queryResult = await this.extensionGalleryService.query({ names: extensionIdentifiers.map(e => e.id), pageSize: extensionIdentifiers.length }, CancellationToken.None); + const items = queryResult.firstPage.map(e => ({ + identifier: e.identifier, + label: e.identifier.id, + description: e.displayName + })); + quickPick.busy = false; + quickPick.items = items; + quickPick.selectedItems = items; + disposables.add(quickPick.onDidAccept(async () => { + const ignoredExtensions: string[] = this.configurationService.getValue('sync.ignoredExtensions').filter(id => quickPick.selectedItems.every(({ identifier }) => !areSameExtensions(identifier, { id }))); + ignoredExtensions.push(...items.filter(item => quickPick.selectedItems.indexOf(item) === -1).map(({ identifier }) => identifier.id)); + await this.configurationService.updateValue('sync.ignoredExtensions', ignoredExtensions); + quickPick.hide(); + })); + disposables.add(quickPick.onDidHide(() => { + disposables.dispose(); + c(); + })); + }); + } } registerSingleton(IUserDataSyncUtilService, UserDataSyncUtilService); diff --git a/src/vs/workbench/services/userDataSync/electron-browser/settingsSyncService.ts b/src/vs/workbench/services/userDataSync/electron-browser/settingsSyncService.ts new file mode 100644 index 0000000000..59687af142 --- /dev/null +++ b/src/vs/workbench/services/userDataSync/electron-browser/settingsSyncService.ts @@ -0,0 +1,96 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SyncStatus, ISettingsSyncService, IConflictSetting } from 'vs/platform/userDataSync/common/userDataSync'; +import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IChannel } from 'vs/base/parts/ipc/common/ipc'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; + +export class SettingsSyncService extends Disposable implements ISettingsSyncService { + + _serviceBrand: undefined; + + private readonly channel: IChannel; + + private _status: SyncStatus = SyncStatus.Uninitialized; + get status(): SyncStatus { return this._status; } + private _onDidChangeStatus: Emitter = this._register(new Emitter()); + readonly onDidChangeStatus: Event = this._onDidChangeStatus.event; + + private _conflicts: IConflictSetting[] = []; + get conflicts(): IConflictSetting[] { return this._conflicts; } + private _onDidChangeConflicts: Emitter = this._register(new Emitter()); + readonly onDidChangeConflicts: Event = this._onDidChangeConflicts.event; + + get onDidChangeLocal(): Event { return this.channel.listen('onDidChangeLocal'); } + + constructor( + @ISharedProcessService sharedProcessService: ISharedProcessService + ) { + super(); + this.channel = sharedProcessService.getChannel('settingsSync'); + this.channel.call('_getInitialStatus').then(status => { + this.updateStatus(status); + this._register(this.channel.listen('onDidChangeStatus')(status => this.updateStatus(status))); + }); + this.channel.call('_getInitialConflicts').then(conflicts => { + if (conflicts.length) { + this.updateConflicts(conflicts); + } + this._register(this.channel.listen('onDidChangeConflicts')(conflicts => this.updateConflicts(conflicts))); + }); + } + + pull(): Promise { + return this.channel.call('pull'); + } + + push(): Promise { + return this.channel.call('push'); + } + + sync(_continue?: boolean): Promise { + return this.channel.call('sync', [_continue]); + } + + stop(): void { + this.channel.call('stop'); + } + + resetLocal(): Promise { + return this.channel.call('resetLocal'); + } + + hasPreviouslySynced(): Promise { + return this.channel.call('hasPreviouslySynced'); + } + + hasRemoteData(): Promise { + return this.channel.call('hasRemoteData'); + } + + hasLocalData(): Promise { + return this.channel.call('hasLocalData'); + } + + resolveConflicts(conflicts: { key: string, value: any | undefined }[]): Promise { + return this.channel.call('resolveConflicts', [conflicts]); + } + + private async updateStatus(status: SyncStatus): Promise { + this._status = status; + this._onDidChangeStatus.fire(status); + } + + private async updateConflicts(conflicts: IConflictSetting[]): Promise { + this._conflicts = conflicts; + this._onDidChangeConflicts.fire(conflicts); + } + +} + +registerSingleton(ISettingsSyncService, SettingsSyncService); diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataAuthTokenService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataAuthTokenService.ts new file mode 100644 index 0000000000..2d04540e6a --- /dev/null +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataAuthTokenService.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IUserDataAuthTokenService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IChannel } from 'vs/base/parts/ipc/common/ipc'; +import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Emitter, Event } from 'vs/base/common/event'; + +export class UserDataAuthTokenService extends Disposable implements IUserDataAuthTokenService { + + _serviceBrand: undefined; + + private readonly channel: IChannel; + private _onDidChangeToken: Emitter = this._register(new Emitter()); + readonly onDidChangeToken: Event = this._onDidChangeToken.event; + + constructor( + @ISharedProcessService sharedProcessService: ISharedProcessService, + ) { + super(); + this.channel = sharedProcessService.getChannel('authToken'); + } + + getToken(): Promise { + return this.channel.call('getToken'); + } + + setToken(token: string | undefined): Promise { + return this.channel.call('setToken', token); + } +} + +registerSingleton(IUserDataAuthTokenService, UserDataAuthTokenService); diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService.ts new file mode 100644 index 0000000000..5dda992a8d --- /dev/null +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; +import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IChannel } from 'vs/base/parts/ipc/common/ipc'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; + +export class UserDataAutoSyncService extends Disposable implements IUserDataAutoSyncService { + + _serviceBrand: undefined; + + private readonly channel: IChannel; + + constructor( + @ISharedProcessService sharedProcessService: ISharedProcessService + ) { + super(); + this.channel = sharedProcessService.getChannel('userDataAutoSync'); + } + + triggerAutoSync(): Promise { + return this.channel.call('triggerAutoSync'); + } + +} + +registerSingleton(IUserDataAutoSyncService, UserDataAutoSyncService); diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts index 3da51020d4..e5df5875d6 100644 --- a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts @@ -38,14 +38,46 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ }); } + pull(): Promise { + return this.channel.call('pull'); + } + + push(): Promise { + return this.channel.call('push'); + } + sync(_continue?: boolean): Promise { return this.channel.call('sync', [_continue]); } + reset(): Promise { + return this.channel.call('reset'); + } + + resetLocal(): Promise { + return this.channel.call('resetLocal'); + } + stop(): void { this.channel.call('stop'); } + hasPreviouslySynced(): Promise { + return this.channel.call('hasPreviouslySynced'); + } + + hasRemoteData(): Promise { + return this.channel.call('hasRemoteData'); + } + + hasLocalData(): Promise { + return this.channel.call('hasLocalData'); + } + + isFirstTimeSyncAndHasUserData(): Promise { + return this.channel.call('isFirstTimeSyncAndHasUserData'); + } + removeExtension(identifier: IExtensionIdentifier): Promise { return this.channel.call('removeExtension', [identifier]); } diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts index 10a2c49b16..5319e9a0b2 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts @@ -8,31 +8,53 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { Event, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { Disposable, IDisposable, toDisposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; -import { TernarySearchTree } from 'vs/base/common/map'; +import { TernarySearchTree, values } from 'vs/base/common/map'; +import { ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; export const enum WorkingCopyCapabilities { /** - * Signals that the working copy participates - * in auto saving as configured by the user. + * Signals that the working copy requires + * additional input when saving, e.g. an + * associated path to save to. */ - AutoSave = 1 << 1 + Untitled = 1 << 1 } export interface IWorkingCopy { - //#region Dirty Tracking + readonly resource: URI; + + readonly capabilities: WorkingCopyCapabilities; + + + //#region Events readonly onDidChangeDirty: Event; + readonly onDidChangeContent: Event; + + //#endregion + + + //#region Dirty Tracking + isDirty(): boolean; //#endregion - readonly resource: URI; + //#region Save / Backup - readonly capabilities: WorkingCopyCapabilities; + save(options?: ISaveOptions): Promise; + + revert(options?: IRevertOptions): Promise; + + hasBackup(): boolean; + + backup(): Promise; + + //#endregion } export const IWorkingCopyService = createDecorator('workingCopyService'); @@ -41,12 +63,26 @@ export interface IWorkingCopyService { _serviceBrand: undefined; - //#region Dirty Tracking + + //#region Events + + readonly onDidRegister: Event; + + readonly onDidUnregister: Event; readonly onDidChangeDirty: Event; + readonly onDidChangeContent: Event; + + //#endregion + + + //#region Dirty Tracking + readonly dirtyCount: number; + readonly dirtyWorkingCopies: IWorkingCopy[]; + readonly hasDirty: boolean; isDirty(resource: URI): boolean; @@ -56,6 +92,10 @@ export interface IWorkingCopyService { //#region Registry + readonly workingCopies: IWorkingCopy[]; + + getWorkingCopies(resource: URI): IWorkingCopy[]; + registerWorkingCopy(workingCopy: IWorkingCopy): IDisposable; //#endregion @@ -65,11 +105,117 @@ export class WorkingCopyService extends Disposable implements IWorkingCopyServic _serviceBrand: undefined; - //#region Dirty Tracking + //#region Events + + private readonly _onDidRegister = this._register(new Emitter()); + readonly onDidRegister = this._onDidRegister.event; + + private readonly _onDidUnregister = this._register(new Emitter()); + readonly onDidUnregister = this._onDidUnregister.event; private readonly _onDidChangeDirty = this._register(new Emitter()); readonly onDidChangeDirty = this._onDidChangeDirty.event; + private readonly _onDidChangeContent = this._register(new Emitter()); + readonly onDidChangeContent = this._onDidChangeContent.event; + + //#endregion + + + //#region Registry + + private mapResourceToWorkingCopy = TernarySearchTree.forPaths>(); + + get workingCopies(): IWorkingCopy[] { return values(this._workingCopies); } + private _workingCopies = new Set(); + + getWorkingCopies(resource: URI): IWorkingCopy[] { + const workingCopies = this.mapResourceToWorkingCopy.get(resource.toString()); + + return workingCopies ? values(workingCopies) : []; + } + + registerWorkingCopy(workingCopy: IWorkingCopy): IDisposable { + const disposables = new DisposableStore(); + + // Registry + let workingCopiesForResource = this.mapResourceToWorkingCopy.get(workingCopy.resource.toString()); + if (!workingCopiesForResource) { + workingCopiesForResource = new Set(); + this.mapResourceToWorkingCopy.set(workingCopy.resource.toString(), workingCopiesForResource); + } + + workingCopiesForResource.add(workingCopy); + + this._workingCopies.add(workingCopy); + + // Wire in Events + disposables.add(workingCopy.onDidChangeContent(() => this._onDidChangeContent.fire(workingCopy))); + disposables.add(workingCopy.onDidChangeDirty(() => this._onDidChangeDirty.fire(workingCopy))); + + // Send some initial events + this._onDidRegister.fire(workingCopy); + if (workingCopy.isDirty()) { + this._onDidChangeDirty.fire(workingCopy); + } + + return toDisposable(() => { + this.unregisterWorkingCopy(workingCopy); + dispose(disposables); + + // Signal as event + this._onDidUnregister.fire(workingCopy); + }); + } + + private unregisterWorkingCopy(workingCopy: IWorkingCopy): void { + + // Remove from registry + const workingCopiesForResource = this.mapResourceToWorkingCopy.get(workingCopy.resource.toString()); + if (workingCopiesForResource && workingCopiesForResource.delete(workingCopy) && workingCopiesForResource.size === 0) { + this.mapResourceToWorkingCopy.delete(workingCopy.resource.toString()); + } + + this._workingCopies.delete(workingCopy); + + // If copy is dirty, ensure to fire an event to signal the dirty change + // (a disposed working copy cannot account for being dirty in our model) + if (workingCopy.isDirty()) { + this._onDidChangeDirty.fire(workingCopy); + } + } + + //#endregion + + + //#region Dirty Tracking + + get hasDirty(): boolean { + for (const workingCopy of this._workingCopies) { + if (workingCopy.isDirty()) { + return true; + } + } + + return false; + } + + get dirtyCount(): number { + let totalDirtyCount = 0; + + for (const workingCopy of this._workingCopies) { + if (workingCopy.isDirty()) { + totalDirtyCount++; + } + } + + return totalDirtyCount; + } + + get dirtyWorkingCopies(): IWorkingCopy[] { + return this.workingCopies.filter(workingCopy => workingCopy.isDirty()); + } + isDirty(resource: URI): boolean { const workingCopies = this.mapResourceToWorkingCopy.get(resource.toString()); if (workingCopies) { @@ -83,79 +229,6 @@ export class WorkingCopyService extends Disposable implements IWorkingCopyServic return false; } - get hasDirty(): boolean { - for (const workingCopy of this.workingCopies) { - if (workingCopy.isDirty()) { - return true; - } - } - - return false; - } - - get dirtyCount(): number { - let totalDirtyCount = 0; - - for (const workingCopy of this.workingCopies) { - if (workingCopy.isDirty()) { - totalDirtyCount++; - } - } - - return totalDirtyCount; - } - - //#endregion - - - //#region Registry - - private mapResourceToWorkingCopy = TernarySearchTree.forPaths>(); - private workingCopies = new Set(); - - registerWorkingCopy(workingCopy: IWorkingCopy): IDisposable { - const disposables = new DisposableStore(); - - // Registry - let workingCopiesForResource = this.mapResourceToWorkingCopy.get(workingCopy.resource.toString()); - if (!workingCopiesForResource) { - workingCopiesForResource = new Set(); - this.mapResourceToWorkingCopy.set(workingCopy.resource.toString(), workingCopiesForResource); - } - - workingCopiesForResource.add(workingCopy); - - this.workingCopies.add(workingCopy); - - // Dirty Events - disposables.add(workingCopy.onDidChangeDirty(() => this._onDidChangeDirty.fire(workingCopy))); - if (workingCopy.isDirty()) { - this._onDidChangeDirty.fire(workingCopy); - } - - return toDisposable(() => { - this.unregisterWorkingCopy(workingCopy); - dispose(disposables); - }); - } - - private unregisterWorkingCopy(workingCopy: IWorkingCopy): void { - - // Remove from registry - const workingCopiesForResource = this.mapResourceToWorkingCopy.get(workingCopy.resource.toString()); - if (workingCopiesForResource && workingCopiesForResource.delete(workingCopy) && workingCopiesForResource.size === 0) { - this.mapResourceToWorkingCopy.delete(workingCopy.resource.toString()); - } - - this.workingCopies.delete(workingCopy); - - // If copy is dirty, ensure to fire an event to signal the dirty change - // (a disposed working copy cannot account for being dirty in our model) - if (workingCopy.isDirty()) { - this._onDidChangeDirty.fire(workingCopy); - } - } - //#endregion } diff --git a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts index c7ecec2381..c3a9fb6480 100644 --- a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts @@ -9,6 +9,7 @@ import { URI } from 'vs/base/common/uri'; import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { TestWorkingCopyService } from 'vs/workbench/test/workbenchTestServices'; +import { ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; suite('WorkingCopyService', () => { @@ -17,6 +18,9 @@ suite('WorkingCopyService', () => { private readonly _onDidChangeDirty = this._register(new Emitter()); readonly onDidChangeDirty = this._onDidChangeDirty.event; + private readonly _onDidChangeContent = this._register(new Emitter()); + readonly onDidChangeContent = this._onDidChangeContent.event; + private readonly _onDispose = this._register(new Emitter()); readonly onDispose = this._onDispose.event; @@ -37,10 +41,28 @@ suite('WorkingCopyService', () => { } } + setContent(content: string): void { + this._onDidChangeContent.fire(); + } + isDirty(): boolean { return this.dirty; } + async save(options?: ISaveOptions): Promise { + return true; + } + + async revert(options?: IRevertOptions): Promise { + this.setDirty(false); + + return true; + } + + async backup(): Promise { } + + hasBackup(): boolean { return false; } + dispose(): void { this._onDispose.fire(); @@ -54,8 +76,18 @@ suite('WorkingCopyService', () => { const onDidChangeDirty: IWorkingCopy[] = []; service.onDidChangeDirty(copy => onDidChangeDirty.push(copy)); + const onDidChangeContent: IWorkingCopy[] = []; + service.onDidChangeContent(copy => onDidChangeContent.push(copy)); + + const onDidRegister: IWorkingCopy[] = []; + service.onDidRegister(copy => onDidRegister.push(copy)); + + const onDidUnregister: IWorkingCopy[] = []; + service.onDidUnregister(copy => onDidUnregister.push(copy)); + assert.equal(service.hasDirty, false); assert.equal(service.dirtyCount, 0); + assert.equal(service.workingCopies.length, 0); assert.equal(service.isDirty(URI.file('/')), false); // resource 1 @@ -63,18 +95,32 @@ suite('WorkingCopyService', () => { const copy1 = new TestWorkingCopy(resource1); const unregister1 = service.registerWorkingCopy(copy1); + assert.equal(service.workingCopies.length, 1); + assert.equal(service.workingCopies[0], copy1); + assert.equal(onDidRegister.length, 1); + assert.equal(onDidRegister[0], copy1); assert.equal(service.dirtyCount, 0); assert.equal(service.isDirty(resource1), false); assert.equal(service.hasDirty, false); copy1.setDirty(true); + assert.equal(copy1.isDirty(), true); assert.equal(service.dirtyCount, 1); + assert.equal(service.dirtyWorkingCopies.length, 1); + assert.equal(service.dirtyWorkingCopies[0], copy1); + assert.equal(service.getWorkingCopies(copy1.resource).length, 1); + assert.equal(service.getWorkingCopies(copy1.resource)[0], copy1); assert.equal(service.isDirty(resource1), true); assert.equal(service.hasDirty, true); assert.equal(onDidChangeDirty.length, 1); assert.equal(onDidChangeDirty[0], copy1); + copy1.setContent('foo'); + + assert.equal(onDidChangeContent.length, 1); + assert.equal(onDidChangeContent[0], copy1); + copy1.setDirty(false); assert.equal(service.dirtyCount, 0); @@ -85,11 +131,17 @@ suite('WorkingCopyService', () => { unregister1.dispose(); + assert.equal(onDidUnregister.length, 1); + assert.equal(onDidUnregister[0], copy1); + assert.equal(service.workingCopies.length, 0); + // resource 2 const resource2 = URI.file('/some/folder/file-dirty.txt'); const copy2 = new TestWorkingCopy(resource2, true); const unregister2 = service.registerWorkingCopy(copy2); + assert.equal(onDidRegister.length, 2); + assert.equal(onDidRegister[1], copy2); assert.equal(service.dirtyCount, 1); assert.equal(service.isDirty(resource2), true); assert.equal(service.hasDirty, true); @@ -97,7 +149,15 @@ suite('WorkingCopyService', () => { assert.equal(onDidChangeDirty.length, 3); assert.equal(onDidChangeDirty[2], copy2); + copy2.setContent('foo'); + + assert.equal(onDidChangeContent.length, 2); + assert.equal(onDidChangeContent[1], copy2); + unregister2.dispose(); + + assert.equal(onDidUnregister.length, 2); + assert.equal(onDidUnregister[1], copy2); assert.equal(service.dirtyCount, 0); assert.equal(service.hasDirty, false); assert.equal(onDidChangeDirty.length, 4); @@ -118,6 +178,10 @@ suite('WorkingCopyService', () => { const copy2 = new TestWorkingCopy(resource); const unregister2 = service.registerWorkingCopy(copy2); + assert.equal(service.getWorkingCopies(copy1.resource).length, 2); + assert.equal(service.getWorkingCopies(copy1.resource)[0], copy1); + assert.equal(service.getWorkingCopies(copy1.resource)[1], copy2); + copy1.setDirty(true); assert.equal(service.dirtyCount, 1); diff --git a/src/vs/workbench/test/browser/parts/views/views.test.ts b/src/vs/workbench/test/browser/parts/views/views.test.ts index 8d5cbc0a64..804bc62aa2 100644 --- a/src/vs/workbench/test/browser/parts/views/views.test.ts +++ b/src/vs/workbench/test/browser/parts/views/views.test.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { ContributableViewsModel, ViewsService, IViewState } from 'vs/workbench/browser/parts/views/views'; -import { IViewsRegistry, IViewDescriptor, IViewContainersRegistry, Extensions as ViewContainerExtensions, IViewsService, ViewContainerLocation } from 'vs/workbench/common/views'; +import { ContributableViewsModel, ViewDescriptorService, IViewState } from 'vs/workbench/browser/parts/views/views'; +import { IViewsRegistry, IViewDescriptor, IViewContainersRegistry, Extensions as ViewContainerExtensions, IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { move } from 'vs/base/common/arrays'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -14,8 +14,9 @@ import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/commo import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { ContextKeyService } from 'vs/platform/contextkey/browser/contextKeyService'; import sinon = require('sinon'); +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -const container = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ id: 'test', name: 'test', ctorDescriptor: { ctor: {} } }, ViewContainerLocation.Sidebar); +const container = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ id: 'test', name: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); const ViewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); class ViewDescriptorSequence { @@ -37,14 +38,14 @@ class ViewDescriptorSequence { suite('ContributableViewsModel', () => { - let viewsService: IViewsService; + let viewDescriptorService: IViewDescriptorService; let contextKeyService: IContextKeyService; setup(() => { const instantiationService: TestInstantiationService = workbenchInstantiationService(); contextKeyService = instantiationService.createInstance(ContextKeyService); instantiationService.stub(IContextKeyService, contextKeyService); - viewsService = instantiationService.createInstance(ViewsService); + viewDescriptorService = instantiationService.createInstance(ViewDescriptorService); }); teardown(() => { @@ -52,12 +53,12 @@ suite('ContributableViewsModel', () => { }); test('empty model', function () { - const model = new ContributableViewsModel(container, viewsService); + const model = new ContributableViewsModel(container, viewDescriptorService); assert.equal(model.visibleViewDescriptors.length, 0); }); test('register/unregister', () => { - const model = new ContributableViewsModel(container, viewsService); + const model = new ContributableViewsModel(container, viewDescriptorService); const seq = new ViewDescriptorSequence(model); assert.equal(model.visibleViewDescriptors.length, 0); @@ -83,7 +84,7 @@ suite('ContributableViewsModel', () => { }); test('when contexts', async function () { - const model = new ContributableViewsModel(container, viewsService); + const model = new ContributableViewsModel(container, viewDescriptorService); const seq = new ViewDescriptorSequence(model); assert.equal(model.visibleViewDescriptors.length, 0); @@ -127,7 +128,7 @@ suite('ContributableViewsModel', () => { }); test('when contexts - multiple', async function () { - const model = new ContributableViewsModel(container, viewsService); + const model = new ContributableViewsModel(container, viewDescriptorService); const seq = new ViewDescriptorSequence(model); const view1: IViewDescriptor = { id: 'view1', ctorDescriptor: null!, name: 'Test View 1' }; @@ -150,7 +151,7 @@ suite('ContributableViewsModel', () => { }); test('when contexts - multiple 2', async function () { - const model = new ContributableViewsModel(container, viewsService); + const model = new ContributableViewsModel(container, viewDescriptorService); const seq = new ViewDescriptorSequence(model); const view1: IViewDescriptor = { id: 'view1', ctorDescriptor: null!, name: 'Test View 1', when: ContextKeyExpr.equals('showview1', true) }; @@ -173,7 +174,7 @@ suite('ContributableViewsModel', () => { }); test('setVisible', () => { - const model = new ContributableViewsModel(container, viewsService); + const model = new ContributableViewsModel(container, viewDescriptorService); const seq = new ViewDescriptorSequence(model); const view1: IViewDescriptor = { id: 'view1', ctorDescriptor: null!, name: 'Test View 1', canToggleVisibility: true }; @@ -218,7 +219,7 @@ suite('ContributableViewsModel', () => { }); test('move', () => { - const model = new ContributableViewsModel(container, viewsService); + const model = new ContributableViewsModel(container, viewDescriptorService); const seq = new ViewDescriptorSequence(model); const view1: IViewDescriptor = { id: 'view1', ctorDescriptor: null!, name: 'Test View 1' }; @@ -249,7 +250,7 @@ suite('ContributableViewsModel', () => { test('view states', async function () { const viewStates = new Map(); viewStates.set('view1', { visibleGlobal: false, collapsed: false, visibleWorkspace: undefined }); - const model = new ContributableViewsModel(container, viewsService, viewStates); + const model = new ContributableViewsModel(container, viewDescriptorService, viewStates); const seq = new ViewDescriptorSequence(model); assert.equal(model.visibleViewDescriptors.length, 0); @@ -269,7 +270,7 @@ suite('ContributableViewsModel', () => { test('view states and when contexts', async function () { const viewStates = new Map(); viewStates.set('view1', { visibleGlobal: false, collapsed: false, visibleWorkspace: undefined }); - const model = new ContributableViewsModel(container, viewsService, viewStates); + const model = new ContributableViewsModel(container, viewDescriptorService, viewStates); const seq = new ViewDescriptorSequence(model); assert.equal(model.visibleViewDescriptors.length, 0); @@ -299,7 +300,7 @@ suite('ContributableViewsModel', () => { test('view states and when contexts multiple views', async function () { const viewStates = new Map(); viewStates.set('view1', { visibleGlobal: false, collapsed: false, visibleWorkspace: undefined }); - const model = new ContributableViewsModel(container, viewsService, viewStates); + const model = new ContributableViewsModel(container, viewDescriptorService, viewStates); const seq = new ViewDescriptorSequence(model); assert.equal(model.visibleViewDescriptors.length, 0); @@ -343,7 +344,7 @@ suite('ContributableViewsModel', () => { }); test('remove event is not triggered if view was hidden and removed', async function () { - const model = new ContributableViewsModel(container, viewsService); + const model = new ContributableViewsModel(container, viewDescriptorService); const seq = new ViewDescriptorSequence(model); const viewDescriptor: IViewDescriptor = { diff --git a/src/vs/workbench/test/common/editor/editor.test.ts b/src/vs/workbench/test/common/editor/editor.test.ts index 9d77f92dcb..7ada967274 100644 --- a/src/vs/workbench/test/common/editor/editor.test.ts +++ b/src/vs/workbench/test/common/editor/editor.test.ts @@ -14,8 +14,7 @@ import { workbenchInstantiationService } from 'vs/workbench/test/workbenchTestSe import { Schemas } from 'vs/base/common/network'; class ServiceAccessor { - constructor(@IUntitledTextEditorService public untitledTextEditorService: UntitledTextEditorService) { - } + constructor(@IUntitledTextEditorService public untitledTextEditorService: UntitledTextEditorService) { } } class FileEditorInput extends EditorInput { @@ -48,7 +47,6 @@ suite('Workbench editor', () => { }); teardown(() => { - accessor.untitledTextEditorService.revertAll(); accessor.untitledTextEditorService.dispose(); }); @@ -57,7 +55,7 @@ suite('Workbench editor', () => { assert.ok(!toResource(null!)); - const untitled = service.createOrGet(); + const untitled = service.create(); assert.equal(toResource(untitled)!.toString(), untitled.getResource().toString()); assert.equal(toResource(untitled, { supportSideBySide: SideBySideEditor.MASTER })!.toString(), untitled.getResource().toString()); diff --git a/src/vs/workbench/test/common/editor/editorOptions.test.ts b/src/vs/workbench/test/common/editor/editorOptions.test.ts deleted file mode 100644 index ce2e0df3a9..0000000000 --- a/src/vs/workbench/test/common/editor/editorOptions.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import { EditorOptions, TextEditorOptions } from 'vs/workbench/common/editor'; - -suite('Workbench editor options', () => { - - test('EditorOptions', () => { - let options = new EditorOptions(); - - assert(!options.preserveFocus); - options.preserveFocus = true; - assert(options.preserveFocus); - assert(!options.forceReload); - options.forceReload = true; - assert(options.forceReload); - - options = new EditorOptions(); - options.forceReload = true; - }); - - test('TextEditorOptions', () => { - let options = new TextEditorOptions(); - let otherOptions = new TextEditorOptions(); - - assert(!options.hasOptionsDefined()); - options.selection(1, 1, 2, 2); - assert(options.hasOptionsDefined()); - - otherOptions.selection(1, 1, 2, 2); - - options = new TextEditorOptions(); - options.forceReload = true; - options.selection(1, 1, 2, 2); - }); -}); \ No newline at end of file diff --git a/src/vs/workbench/test/common/editor/untitledTextEditor.test.ts b/src/vs/workbench/test/common/editor/untitledTextEditor.test.ts index e4c373b4fc..26447dd4a5 100644 --- a/src/vs/workbench/test/common/editor/untitledTextEditor.test.ts +++ b/src/vs/workbench/test/common/editor/untitledTextEditor.test.ts @@ -9,28 +9,22 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti 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 } from 'vs/workbench/test/workbenchTestServices'; -import { UntitledTextEditorModel } from 'vs/workbench/common/editor/untitledTextEditorModel'; +import { workbenchInstantiationService, TestEditorService } from 'vs/workbench/test/workbenchTestServices'; import { IModeService } from 'vs/editor/common/services/modeService'; import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl'; -import { UntitledTextEditorInput } from 'vs/workbench/common/editor/untitledTextEditorInput'; -import { timeout } from 'vs/base/common/async'; 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'; - -export class TestUntitledTextEditorService extends UntitledTextEditorService { - get(resource: URI) { return super.get(resource); } - getAll(resources?: URI[]): UntitledTextEditorInput[] { return super.getAll(resources); } -} +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; class ServiceAccessor { constructor( - @IUntitledTextEditorService public readonly untitledTextEditorService: TestUntitledTextEditorService, + @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) { - } + @IConfigurationService public readonly testConfigurationService: TestConfigurationService + ) { } } suite('Workbench untitled text editors', () => { @@ -44,63 +38,52 @@ suite('Workbench untitled text editors', () => { }); teardown(() => { - accessor.untitledTextEditorService.revertAll(); - accessor.untitledTextEditorService.dispose(); + (accessor.untitledTextEditorService as UntitledTextEditorService).dispose(); }); test('Untitled Text Editor Service', async (done) => { const service = accessor.untitledTextEditorService; const workingCopyService = accessor.workingCopyService; - assert.equal(service.getAll().length, 0); - - let createdResources: URI[] = []; - const createListener = service.onDidCreate(resource => { - createdResources.push(resource); - }); - - const input1 = service.createOrGet(); - assert.equal(input1, service.createOrGet(input1.getResource())); + const input1 = service.create(); + assert.equal(input1, service.create({ untitledResource: input1.getResource() })); + assert.equal(service.get(input1.getResource()), input1); assert.ok(service.exists(input1.getResource())); assert.ok(!service.exists(URI.file('testing'))); - assert.equal(createdResources.length, 1); - assert.equal(createdResources[0].toString(), input1.getResource()); - createListener.dispose(); + const input2 = service.create(); + assert.equal(service.get(input2.getResource()), input2); - const input2 = service.createOrGet(); - - // get() / getAll() + // get() assert.equal(service.get(input1.getResource()), input1); - assert.equal(service.getAll().length, 2); - assert.equal(service.getAll([input1.getResource(), input2.getResource()]).length, 2); + assert.equal(service.get(input2.getResource()), input2); - // revertAll() - service.revertAll([input1.getResource()]); + // revert() + input1.revert(); assert.ok(input1.isDisposed()); - assert.equal(service.getAll().length, 1); + assert.ok(!service.get(input1.getResource())); // dirty const model = await input2.resolve(); + assert.equal(await service.resolve({ untitledResource: input2.getResource() }), model); - assert.ok(!service.isDirty(input2.getResource())); + assert.ok(!input2.isDirty()); const listener = service.onDidChangeDirty(resource => { listener.dispose(); assert.equal(resource.toString(), input2.getResource().toString()); - assert.ok(service.isDirty(input2.getResource())); - assert.equal(service.getDirty()[0].toString(), input2.getResource().toString()); - assert.equal(service.getDirty([input2.getResource()])[0].toString(), input2.getResource().toString()); - assert.equal(service.getDirty([input1.getResource()]).length, 0); + assert.ok(input2.isDirty()); assert.ok(workingCopyService.isDirty(input2.getResource())); assert.equal(workingCopyService.dirtyCount, 1); - service.revertAll(); - assert.equal(service.getAll().length, 0); + input1.revert(); + input2.revert(); + assert.ok(!service.get(input1.getResource())); + assert.ok(!service.get(input2.getResource())); assert.ok(!input2.isDirty()); assert.ok(!model.isDirty()); @@ -118,22 +101,26 @@ suite('Workbench untitled text editors', () => { }); model.textEditorModel.setValue('foo bar'); + model.dispose(); + input1.dispose(); + input2.dispose(); }); - test('Untitled with associated resource', () => { + test('Untitled with associated resource is dirty', () => { const service = accessor.untitledTextEditorService; const file = URI.file(join('C:\\', '/foo/file.txt')); - const untitled = service.createOrGet(file); + const untitled = service.create({ associatedResource: file }); assert.ok(service.hasAssociatedFilePath(untitled.getResource())); + assert.equal(untitled.isDirty(), true); untitled.dispose(); }); - test('Untitled no longer dirty when content gets empty', async () => { + test('Untitled no longer dirty when content gets empty (not with associated resource)', async () => { const service = accessor.untitledTextEditorService; const workingCopyService = accessor.workingCopyService; - const input = service.createOrGet(); + const input = service.create(); // dirty const model = await input.resolve(); @@ -144,12 +131,13 @@ suite('Workbench untitled text editors', () => { assert.ok(!model.isDirty()); assert.ok(!workingCopyService.isDirty(model.resource)); input.dispose(); + model.dispose(); }); - test('Untitled via loadOrCreate', async () => { + test('Untitled via create options', async () => { const service = accessor.untitledTextEditorService; - const model1 = await service.loadOrCreate(); + const model1 = await service.create().resolve(); model1.textEditorModel!.setValue('foo bar'); assert.ok(model1.isDirty()); @@ -157,17 +145,17 @@ suite('Workbench untitled text editors', () => { model1.textEditorModel!.setValue(''); assert.ok(!model1.isDirty()); - const model2 = await service.loadOrCreate({ initialValue: 'Hello World' }); + const model2 = await service.create({ initialValue: 'Hello World' }).resolve(); assert.equal(snapshotToString(model2.createSnapshot()!), 'Hello World'); - const input = service.createOrGet(); + const input = service.create(); - const model3 = await service.loadOrCreate({ resource: input.getResource() }); + const model3 = await service.create({ untitledResource: input.getResource() }).resolve(); assert.equal(model3.resource.toString(), input.getResource().toString()); const file = URI.file(join('C:\\', '/foo/file44.txt')); - const model4 = await service.loadOrCreate({ resource: file }); + const model4 = await service.create({ associatedResource: file }).resolve(); assert.ok(service.hasAssociatedFilePath(model4.resource)); assert.ok(model4.isDirty()); @@ -180,15 +168,16 @@ suite('Workbench untitled text editors', () => { test('Untitled suggest name', function () { const service = accessor.untitledTextEditorService; - const input = service.createOrGet(); + const input = service.create(); - assert.ok(service.suggestFileName(input.getResource())); + assert.ok(input.suggestFileName().length > 0); + input.dispose(); }); test('Untitled with associated path remains dirty when content gets empty', async () => { const service = accessor.untitledTextEditorService; const file = URI.file(join('C:\\', '/foo/file.txt')); - const input = service.createOrGet(file); + const input = service.create({ associatedResource: file }); // dirty const model = await input.resolve(); @@ -197,26 +186,30 @@ suite('Workbench untitled text editors', () => { model.textEditorModel.setValue(''); assert.ok(model.isDirty()); input.dispose(); + model.dispose(); }); test('Untitled with initial content is dirty', async () => { const service = accessor.untitledTextEditorService; - const input = service.createOrGet(undefined, undefined, 'Hello World'); const workingCopyService = accessor.workingCopyService; + const untitled = service.create({ initialValue: 'Hello World' }); + assert.equal(untitled.isDirty(), true); + let onDidChangeDirty: IWorkingCopy | undefined = undefined; const listener = workingCopyService.onDidChangeDirty(copy => { onDidChangeDirty = copy; }); // dirty - const model = await input.resolve(); + const model = await untitled.resolve(); assert.ok(model.isDirty()); assert.equal(workingCopyService.dirtyCount, 1); assert.equal(onDidChangeDirty, model); - input.dispose(); + untitled.dispose(); listener.dispose(); + model.dispose(); }); test('Untitled created with files.defaultLanguage setting', () => { @@ -225,7 +218,7 @@ suite('Workbench untitled text editors', () => { config.setUserConfiguration('files', { 'defaultLanguage': defaultLanguage }); const service = accessor.untitledTextEditorService; - const input = service.createOrGet(); + const input = service.create(); assert.equal(input.getMode(), defaultLanguage); @@ -234,6 +227,23 @@ suite('Workbench untitled text editors', () => { input.dispose(); }); + test('Untitled created with files.defaultLanguage setting (${activeEditorLanguage})', () => { + const config = accessor.testConfigurationService; + config.setUserConfiguration('files', { 'defaultLanguage': '${activeEditorLanguage}' }); + + accessor.editorService.activeTextEditorMode = 'typescript'; + + const service = accessor.untitledTextEditorService; + const input = service.create(); + + assert.equal(input.getMode(), 'typescript'); + + config.setUserConfiguration('files', { 'defaultLanguage': undefined }); + accessor.editorService.activeTextEditorMode = undefined; + + input.dispose(); + }); + test('Untitled created with mode overrides files.defaultLanguage setting', () => { const mode = 'typescript'; const defaultLanguage = 'javascript'; @@ -241,7 +251,7 @@ suite('Workbench untitled text editors', () => { config.setUserConfiguration('files', { 'defaultLanguage': defaultLanguage }); const service = accessor.untitledTextEditorService; - const input = service.createOrGet(null!, mode); + const input = service.create({ mode }); assert.equal(input.getMode(), mode); @@ -258,7 +268,7 @@ suite('Workbench untitled text editors', () => { }); const service = accessor.untitledTextEditorService; - const input = service.createOrGet(null!, mode); + const input = service.create({ mode }); assert.equal(input.getMode(), mode); @@ -270,11 +280,12 @@ suite('Workbench untitled text editors', () => { assert.equal(input.getMode(), PLAINTEXT_MODE_ID); input.dispose(); + model.dispose(); }); test('encoding change event', async () => { const service = accessor.untitledTextEditorService; - const input = service.createOrGet(); + const input = service.create(); let counter = 0; @@ -288,49 +299,58 @@ suite('Workbench untitled text editors', () => { model.setEncoding('utf16'); assert.equal(counter, 1); input.dispose(); + model.dispose(); }); - test('onDidChangeContent event', async () => { + test('onDidChangeContent event', async function () { const service = accessor.untitledTextEditorService; - const input = service.createOrGet(); - - UntitledTextEditorModel.DEFAULT_CONTENT_CHANGE_BUFFER_DELAY = 0; + const input = service.create(); let counter = 0; - service.onDidChangeContent(r => { - counter++; - assert.equal(r.toString(), input.getResource().toString()); - }); - const model = await input.resolve(); - model.textEditorModel.setValue('foo'); - assert.equal(counter, 0, 'Dirty model should not trigger event immediately'); + model.onDidChangeContent(() => counter++); + + model.textEditorModel.setValue('foo'); - await timeout(3); assert.equal(counter, 1, 'Dirty model should trigger event'); model.textEditorModel.setValue('bar'); - await timeout(3); assert.equal(counter, 2, 'Content change when dirty should trigger event'); model.textEditorModel.setValue(''); - await timeout(3); assert.equal(counter, 3, 'Manual revert should trigger event'); model.textEditorModel.setValue('foo'); - await timeout(3); assert.equal(counter, 4, 'Dirty model should trigger event'); - model.revert(); - await timeout(3); - assert.equal(counter, 5, 'Revert should trigger event'); input.dispose(); + model.dispose(); + }); + + test('onDidChangeDirty event', async function () { + const service = accessor.untitledTextEditorService; + const input = service.create(); + + let counter = 0; + + const model = await input.resolve(); + model.onDidChangeDirty(() => counter++); + + model.textEditorModel.setValue('foo'); + + assert.equal(counter, 1, 'Dirty model should trigger event'); + model.textEditorModel.setValue('bar'); + + assert.equal(counter, 1, 'Another change does not fire event'); + + input.dispose(); + model.dispose(); }); test('onDidDisposeModel event', async () => { const service = accessor.untitledTextEditorService; - const input = service.createOrGet(); + const input = service.create(); let counter = 0; @@ -339,9 +359,10 @@ suite('Workbench untitled text editors', () => { assert.equal(r.toString(), input.getResource().toString()); }); - await input.resolve(); + const model = await input.resolve(); assert.equal(counter, 0); input.dispose(); assert.equal(counter, 1); + model.dispose(); }); }); diff --git a/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts b/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts index 85dd8155d7..a5a80b0639 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts @@ -23,7 +23,7 @@ import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { MainContext, ExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostDiagnostics } from 'vs/workbench/api/common/extHostDiagnostics'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import 'vs/workbench/contrib/search/browser/search.contribution'; import { NullLogService } from 'vs/platform/log/common/log'; @@ -32,6 +32,7 @@ import { nullExtensionDescription } from 'vs/workbench/services/extensions/commo import { dispose } from 'vs/base/common/lifecycle'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; import { mock } from 'vs/workbench/test/electron-browser/api/mock'; +import { NullApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; const defaultSelector = { scheme: 'far' }; const model: ITextModel = EditorModel.createFromString( @@ -122,7 +123,7 @@ suite('ExtHostLanguageFeatureCommands', function () { const diagnostics = new ExtHostDiagnostics(rpcProtocol, new NullLogService()); rpcProtocol.set(ExtHostContext.ExtHostDiagnostics, diagnostics); - extHost = new ExtHostLanguageFeatures(rpcProtocol, null, extHostDocuments, commands, diagnostics, new NullLogService()); + extHost = new ExtHostLanguageFeatures(rpcProtocol, null, extHostDocuments, commands, diagnostics, new NullLogService(), NullApiDeprecationService); rpcProtocol.set(ExtHostContext.ExtHostLanguageFeatures, extHost); mainThread = rpcProtocol.set(MainContext.MainThreadLanguageFeatures, inst.createInstance(MainThreadLanguageFeatures, rpcProtocol)); diff --git a/src/vs/workbench/test/electron-browser/api/extHostDiagnostics.test.ts b/src/vs/workbench/test/electron-browser/api/extHostDiagnostics.test.ts index 1044f429d8..4015b60927 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostDiagnostics.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostDiagnostics.test.ts @@ -33,8 +33,7 @@ suite('ExtHostDiagnostics', () => { assert.throws(() => collection.name); assert.throws(() => collection.clear()); assert.throws(() => collection.delete(URI.parse('aa:bb'))); - // tslint:disable-next-line:semicolon - assert.throws(() => collection.forEach(() => { ; })); + assert.throws(() => collection.forEach(() => { })); assert.throws(() => collection.get(URI.parse('aa:bb'))); assert.throws(() => collection.has(URI.parse('aa:bb'))); assert.throws(() => collection.set(URI.parse('aa:bb'), [])); diff --git a/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts b/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts index e54beed669..8cd473004a 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts @@ -11,10 +11,10 @@ import { MainThreadTextEditorsShape, IWorkspaceEditDto } from 'vs/workbench/api/ import { ExtHostDocumentSaveParticipant } from 'vs/workbench/api/common/extHostDocumentSaveParticipant'; import { SingleProxyRPCProtocol } from './testRPCProtocol'; import { SaveReason } from 'vs/workbench/common/editor'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { mock } from 'vs/workbench/test/electron-browser/api/mock'; import { NullLogService } from 'vs/platform/log/common/log'; -import { isResourceTextEdit, ResourceTextEdit } from 'vs/editor/common/modes'; +import { WorkspaceTextEdit } from 'vs/editor/common/modes'; import { timeout } from 'vs/base/common/async'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; @@ -279,8 +279,8 @@ suite('ExtHostDocumentSaveParticipant', () => { sub.dispose(); assert.equal(dto.edits.length, 1); - assert.ok(isResourceTextEdit(dto.edits[0])); - assert.equal((dto.edits[0]).edits.length, 2); + assert.ok(WorkspaceTextEdit.is(dto.edits[0])); + assert.equal((dto.edits[0]).edits.length, 2); }); }); @@ -326,7 +326,7 @@ suite('ExtHostDocumentSaveParticipant', () => { $tryApplyWorkspaceEdit(dto: IWorkspaceEditDto) { for (const edit of dto.edits) { - if (!isResourceTextEdit(edit)) { + if (!WorkspaceTextEdit.is(edit)) { continue; } const { resource, edits } = edit; diff --git a/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts b/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts index 68488128c4..60ad8afd9c 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts @@ -35,7 +35,7 @@ import { getDocumentFormattingEditsUntilResult, getDocumentRangeFormattingEditsU import { getLinks } from 'vs/editor/contrib/links/getLinks'; import { MainContext, ExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostDiagnostics } from 'vs/workbench/api/common/extHostDiagnostics'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { NullLogService } from 'vs/platform/log/common/log'; import { ITextModel, EndOfLineSequence } from 'vs/editor/common/model'; @@ -47,6 +47,7 @@ import { mock } from 'vs/workbench/test/electron-browser/api/mock'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; import { dispose } from 'vs/base/common/lifecycle'; import { withNullAsUndefined } from 'vs/base/common/types'; +import { NullApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; const defaultSelector = { scheme: 'far' }; const model: ITextModel = EditorModel.createFromString( @@ -105,7 +106,7 @@ suite('ExtHostLanguageFeatures', function () { const diagnostics = new ExtHostDiagnostics(rpcProtocol, new NullLogService()); rpcProtocol.set(ExtHostContext.ExtHostDiagnostics, diagnostics); - extHost = new ExtHostLanguageFeatures(rpcProtocol, null, extHostDocuments, commands, diagnostics, new NullLogService()); + extHost = new ExtHostLanguageFeatures(rpcProtocol, null, extHostDocuments, commands, diagnostics, new NullLogService(), NullApiDeprecationService); rpcProtocol.set(ExtHostContext.ExtHostLanguageFeatures, extHost); mainThread = rpcProtocol.set(MainContext.MainThreadLanguageFeatures, inst.createInstance(MainThreadLanguageFeatures, rpcProtocol)); @@ -589,7 +590,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const { validActions: actions } = await getCodeActions(model, model.getFullModelRange(), { type: 'manual' }, CancellationToken.None); + const { validActions: actions } = await getCodeActions(model, model.getFullModelRange(), { type: modes.CodeActionTriggerType.Manual }, CancellationToken.None); assert.equal(actions.length, 2); const [first, second] = actions; assert.equal(first.title, 'Testing1'); @@ -613,7 +614,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const { validActions: actions } = await getCodeActions(model, model.getFullModelRange(), { type: 'manual' }, CancellationToken.None); + const { validActions: actions } = await getCodeActions(model, model.getFullModelRange(), { type: modes.CodeActionTriggerType.Manual }, CancellationToken.None); assert.equal(actions.length, 1); const [first] = actions; assert.equal(first.title, 'Testing1'); @@ -636,7 +637,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const { validActions: actions } = await getCodeActions(model, model.getFullModelRange(), { type: 'manual' }, CancellationToken.None); + const { validActions: actions } = await getCodeActions(model, model.getFullModelRange(), { type: modes.CodeActionTriggerType.Manual }, CancellationToken.None); assert.equal(actions.length, 1); }); @@ -654,7 +655,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const { validActions: actions } = await getCodeActions(model, model.getFullModelRange(), { type: 'manual' }, CancellationToken.None); + const { validActions: actions } = await getCodeActions(model, model.getFullModelRange(), { type: modes.CodeActionTriggerType.Manual }, CancellationToken.None); assert.equal(actions.length, 1); }); @@ -758,8 +759,8 @@ suite('ExtHostLanguageFeatures', function () { const value = await rename(model, new EditorPosition(1, 1), 'newName'); // least relevant rename provider assert.equal(value.edits.length, 2); - assert.equal((value.edits[0]).edits.length, 1); - assert.equal((value.edits[1]).edits.length, 1); + assert.equal((value.edits[0]).edits.length, 1); + assert.equal((value.edits[1]).edits.length, 1); }); // --- parameter hints diff --git a/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts b/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts index 2a1fb66abb..995933e1c0 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts @@ -16,7 +16,7 @@ import { NativeExtHostSearch } from 'vs/workbench/api/node/extHostSearch'; import { Range } from 'vs/workbench/api/common/extHostTypes'; import { IFileMatch, IFileQuery, IPatternInfo, IRawFileMatch2, ISearchCompleteStats, ISearchQuery, ITextQuery, QueryType, resultIsMatch } from 'vs/workbench/services/search/common/search'; import { TestRPCProtocol } from 'vs/workbench/test/electron-browser/api/testRPCProtocol'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { NullLogService } from 'vs/platform/log/common/log'; import { URITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService'; import { mock } from 'vs/workbench/test/electron-browser/api/mock'; diff --git a/src/vs/workbench/test/electron-browser/api/extHostTextEditors.test.ts b/src/vs/workbench/test/electron-browser/api/extHostTextEditors.test.ts index 4b49ab6e39..8899350f77 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostTextEditors.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostTextEditors.test.ts @@ -10,7 +10,7 @@ import { mock } from 'vs/workbench/test/electron-browser/api/mock'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { SingleProxyRPCProtocol, TestRPCProtocol } from 'vs/workbench/test/electron-browser/api/testRPCProtocol'; import { ExtHostEditors } from 'vs/workbench/api/common/extHostTextEditors'; -import { ResourceTextEdit } from 'vs/editor/common/modes'; +import { WorkspaceTextEdit } from 'vs/editor/common/modes'; import { NullLogService } from 'vs/platform/log/common/log'; suite('ExtHostTextEditors.applyWorkspaceEdit', () => { @@ -48,7 +48,7 @@ suite('ExtHostTextEditors.applyWorkspaceEdit', () => { edit.replace(resource, new extHostTypes.Range(0, 0, 0, 0), 'hello'); await editors.applyWorkspaceEdit(edit); assert.equal(workspaceResourceEdits.edits.length, 1); - assert.equal((workspaceResourceEdits.edits[0]).modelVersionId, 1337); + assert.equal((workspaceResourceEdits.edits[0]).modelVersionId, 1337); }); test('does not use version id if document is not available', async () => { @@ -56,7 +56,7 @@ suite('ExtHostTextEditors.applyWorkspaceEdit', () => { edit.replace(URI.parse('foo:bar2'), new extHostTypes.Range(0, 0, 0, 0), 'hello'); await editors.applyWorkspaceEdit(edit); assert.equal(workspaceResourceEdits.edits.length, 1); - assert.ok(typeof (workspaceResourceEdits.edits[0]).modelVersionId === 'undefined'); + assert.ok(typeof (workspaceResourceEdits.edits[0]).modelVersionId === 'undefined'); }); }); diff --git a/src/vs/workbench/test/electron-browser/api/extHostWebview.test.ts b/src/vs/workbench/test/electron-browser/api/extHostWebview.test.ts index 91146c2e75..b4c253971a 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostWebview.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostWebview.test.ts @@ -11,7 +11,7 @@ import { MainThreadWebviews } from 'vs/workbench/api/browser/mainThreadWebview'; import { ExtHostWebviews } from 'vs/workbench/api/common/extHostWebview'; import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; import { mock } from 'vs/workbench/test/electron-browser/api/mock'; -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; import { SingleProxyRPCProtocol } from './testRPCProtocol'; suite('ExtHostWebview', () => { diff --git a/src/vs/workbench/test/electron-browser/api/mainThreadDocumentsAndEditors.test.ts b/src/vs/workbench/test/electron-browser/api/mainThreadDocumentsAndEditors.test.ts index 29d1f039b5..8cc55cb601 100644 --- a/src/vs/workbench/test/electron-browser/api/mainThreadDocumentsAndEditors.test.ts +++ b/src/vs/workbench/test/electron-browser/api/mainThreadDocumentsAndEditors.test.ts @@ -48,10 +48,10 @@ suite('MainThreadDocumentsAndEditors', () => { codeEditorService = new TestCodeEditorService(); textFileService = new class extends mock() { isDirty() { return false; } - models = { - onModelSaved: Event.None, - onModelReverted: Event.None, - onModelDirty: Event.None, + files = { + onDidSave: Event.None, + onDidRevert: Event.None, + onDidChangeDirty: Event.None }; }; const workbenchEditorService = new TestEditorService(); @@ -61,7 +61,6 @@ suite('MainThreadDocumentsAndEditors', () => { onAfterOperation = Event.None; }; - /* tslint:disable */ new MainThreadDocumentsAndEditors( SingleProxyRPCProtocol(new class extends mock() { $acceptDocumentsAndEditorsDelta(delta: IDocumentsAndEditorsDelta) { deltas.push(delta); } @@ -70,10 +69,8 @@ suite('MainThreadDocumentsAndEditors', () => { textFileService, workbenchEditorService, codeEditorService, - null!, fileService, null!, - null!, editorGroupService, null!, new class extends mock() implements IPanelService { @@ -86,7 +83,6 @@ suite('MainThreadDocumentsAndEditors', () => { }, TestEnvironmentService ); - /* tslint:enable */ }); diff --git a/src/vs/workbench/test/electron-browser/api/mainThreadEditors.test.ts b/src/vs/workbench/test/electron-browser/api/mainThreadEditors.test.ts index 5c50a8a9b7..02f0429fd8 100644 --- a/src/vs/workbench/test/electron-browser/api/mainThreadEditors.test.ts +++ b/src/vs/workbench/test/electron-browser/api/mainThreadEditors.test.ts @@ -20,7 +20,7 @@ import { Position } from 'vs/editor/common/core/position'; import { IModelService } from 'vs/editor/common/services/modelService'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { TestFileService, TestEditorService, TestEditorGroupsService, TestEnvironmentService, TestContextService, TestTextResourcePropertiesService } from 'vs/workbench/test/workbenchTestServices'; -import { ResourceTextEdit } from 'vs/editor/common/modes'; +import { WorkspaceTextEdit } from 'vs/editor/common/modes'; import { BulkEditService } from 'vs/workbench/services/bulkEdit/browser/bulkEditService'; import { NullLogService } from 'vs/platform/log/common/log'; import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; @@ -72,10 +72,10 @@ suite('MainThreadEditors', () => { copiedResources.set(source, target); return Promise.resolve(Object.create(null)); } - models = { - onModelSaved: Event.None, - onModelReverted: Event.None, - onModelDirty: Event.None, + files = { + onDidSave: Event.None, + onDidRevert: Event.None, + onDidChangeDirty: Event.None }; }; const workbenchEditorService = new TestEditorService(); @@ -112,10 +112,8 @@ suite('MainThreadEditors', () => { textFileService, workbenchEditorService, codeEditorService, - null!, fileService, null!, - null!, editorGroupService, bulkEditService, new class extends mock() implements IPanelService { @@ -143,7 +141,7 @@ suite('MainThreadEditors', () => { let model = modelService.createModel('something', null, resource); - let workspaceResourceEdit: ResourceTextEdit = { + let workspaceResourceEdit: WorkspaceTextEdit = { resource: resource, modelVersionId: model.getVersionId(), edits: [{ @@ -164,7 +162,7 @@ suite('MainThreadEditors', () => { let model = modelService.createModel('something', null, resource); - let workspaceResourceEdit1: ResourceTextEdit = { + let workspaceResourceEdit1: WorkspaceTextEdit = { resource: resource, modelVersionId: model.getVersionId(), edits: [{ @@ -172,7 +170,7 @@ suite('MainThreadEditors', () => { range: new Range(1, 1, 1, 1) }] }; - let workspaceResourceEdit2: ResourceTextEdit = { + let workspaceResourceEdit2: WorkspaceTextEdit = { resource: resource, modelVersionId: model.getVersionId(), edits: [{ diff --git a/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts b/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts index 2c4e4cef9b..81bfba9077 100644 --- a/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts +++ b/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts @@ -33,7 +33,7 @@ suite('MainThreadSaveParticipant', function () { }); teardown(() => { - (accessor.textFileService.models).clear(); + (accessor.textFileService.files).dispose(); TextFileEditorModel.setSaveParticipant(null); // reset any set participant }); 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 44c1a412e1..551c836e08 100644 --- a/src/vs/workbench/test/electron-browser/quickopen.perf.integrationTest.ts +++ b/src/vs/workbench/test/electron-browser/quickopen.perf.integrationTest.ts @@ -49,7 +49,7 @@ namespace Timer { } } -declare var __dirname: string; +// declare var __dirname: string; // Checkout sources to run against: // git clone --separate-git-dir=testGit --no-checkout --single-branch https://chromium.googlesource.com/chromium/src testWorkspace 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 27e07788e0..49b64ad486 100644 --- a/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts +++ b/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts @@ -36,7 +36,7 @@ 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'; -declare var __dirname: string; +// declare var __dirname: string; // Checkout sources to run against: // git clone --separate-git-dir=testGit --no-checkout --single-branch https://chromium.googlesource.com/chromium/src testWorkspace diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index 3223a1206e..062dcb62d6 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -190,50 +190,40 @@ export class TestContextService implements IWorkspaceContextService { } export class TestTextFileService extends NativeTextFileService { - cleanupBackupsBeforeShutdownCalled!: boolean; - private promptPath!: URI; private resolveTextContentError!: FileOperationError | null; constructor( - @IWorkspaceContextService contextService: IWorkspaceContextService, @IFileService protected fileService: IFileService, @IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorService, @ILifecycleService lifecycleService: ILifecycleService, @IInstantiationService instantiationService: IInstantiationService, - @IModeService modeService: IModeService, @IModelService modelService: IModelService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, - @INotificationService notificationService: INotificationService, - @IBackupFileService backupFileService: IBackupFileService, @IHistoryService historyService: IHistoryService, @IDialogService dialogService: IDialogService, @IFileDialogService fileDialogService: IFileDialogService, @IEditorService editorService: IEditorService, @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, - @IElectronService electronService: IElectronService, @IProductService productService: IProductService, - @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService + @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, + @ITextModelService textModelService: ITextModelService ) { super( - contextService, fileService, untitledTextEditorService, lifecycleService, instantiationService, - modeService, modelService, environmentService, - notificationService, - backupFileService, historyService, dialogService, fileDialogService, editorService, textResourceConfigurationService, - electronService, productService, - filesConfigurationService + filesConfigurationService, + textModelService ); } @@ -269,11 +259,6 @@ export class TestTextFileService extends NativeTextFileService { promptForPath(_resource: URI, _defaultPath: URI): Promise { return Promise.resolve(this.promptPath); } - - protected cleanupBackupsBeforeShutdown(): Promise { - this.cleanupBackupsBeforeShutdownCalled = true; - return Promise.resolve(); - } } export interface ITestInstantiationService extends IInstantiationService { @@ -426,7 +411,7 @@ export class TestFileDialogService implements IFileDialogService { pickWorkspaceAndOpen(_options: IPickAndOpenOptions): Promise { return Promise.resolve(0); } - pickFileToSave(_options: ISaveDialogOptions): Promise { + pickFileToSave(defaultUri: URI, availableFileSystems?: string[]): Promise { return Promise.resolve(undefined); } showSaveDialog(_options: ISaveDialogOptions): Promise { @@ -905,6 +890,7 @@ export class TestEditorService implements EditorServiceImpl { activeControl!: IVisibleEditor; activeTextEditorWidget: any; + activeTextEditorMode: any; activeEditor!: IEditorInput; editors: ReadonlyArray = []; mostRecentlyActiveEditors: ReadonlyArray = []; @@ -1222,7 +1208,11 @@ export class TestBackupFileService implements IBackupFileService { return Promise.resolve(); } + didDiscardAllWorkspaceBackups = false; + discardAllWorkspaceBackups(): Promise { + this.didDiscardAllWorkspaceBackups = true; + return Promise.resolve(); } } diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 36b9ffb29a..20fc701c1f 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -72,7 +72,7 @@ import 'vs/workbench/services/history/browser/history'; import 'vs/workbench/services/activity/browser/activityService'; import 'vs/workbench/services/keybinding/browser/keybindingService'; import 'vs/workbench/services/untitled/common/untitledTextEditorService'; -import 'vs/workbench/services/textfile/common/textResourcePropertiesService'; +import 'vs/workbench/services/textresourceProperties/common/textResourcePropertiesService'; import 'vs/workbench/services/mode/common/workbenchModeService'; import 'vs/workbench/services/commands/common/commandService'; import 'vs/workbench/services/themes/browser/workbenchThemeService'; @@ -253,6 +253,9 @@ import 'vs/workbench/contrib/files/browser/files.contribution'; // Backup import 'vs/workbench/contrib/backup/common/backup.contribution'; +// bulkEdit +import 'vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution'; + // Search import 'vs/workbench/contrib/search/browser/search.contribution'; import 'vs/workbench/contrib/search/browser/searchView'; diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index 59b9822a26..a872041fbe 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -51,7 +51,10 @@ import 'vs/workbench/services/url/electron-browser/urlService'; import 'vs/workbench/services/workspaces/electron-browser/workspacesService'; import 'vs/workbench/services/workspaces/electron-browser/workspaceEditingService'; import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncService'; -import 'vs/workbench/services/authToken/electron-browser/authTokenService'; +import 'vs/workbench/services/userDataSync/electron-browser/settingsSyncService'; +import 'vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService'; +import 'vs/workbench/services/userDataSync/electron-browser/userDataAuthTokenService'; +import 'vs/workbench/services/authentication/browser/authenticationService'; import 'vs/workbench/services/host/electron-browser/desktopHostService'; import 'vs/workbench/services/request/electron-browser/requestService'; import 'vs/workbench/services/lifecycle/electron-browser/lifecycleService'; @@ -104,6 +107,9 @@ import 'vs/workbench/contrib/splash/electron-browser/partsSplash.contribution'; import 'vs/workbench/contrib/files/electron-browser/files.contribution'; import 'vs/workbench/contrib/files/electron-browser/fileActions.contribution'; +// Backup +import 'vs/workbench/contrib/backup/electron-browser/backup.contribution'; + // Debug // import 'vs/workbench/contrib/debug/node/debugHelperService'; {{SQL CARBON EDIT}} import 'vs/workbench/contrib/debug/electron-browser/extensionHostDebugService'; diff --git a/src/vs/workbench/workbench.web.api.ts b/src/vs/workbench/workbench.web.api.ts index 939746a338..cfa984cdbd 100644 --- a/src/vs/workbench/workbench.web.api.ts +++ b/src/vs/workbench/workbench.web.api.ts @@ -34,6 +34,26 @@ interface IExternalUriResolver { (uri: URI): Promise; } +interface TunnelOptions { + remoteAddress: { port: number, host: string }; + // The desired local port. If this port can't be used, then another will be chosen. + localAddressPort?: number; + label?: string; +} + +interface Tunnel { + remoteAddress: { port: number, host: string }; + //The complete local address(ex. localhost:1234) + localAddress: string; + // Implementers of Tunnel should fire onDidDispose when dispose is called. + onDidDispose: Event; + dispose(): void; +} + +interface ITunnelFactory { + (tunnelOptions: TunnelOptions): Thenable | undefined; +} + interface IWorkbenchConstructionOptions { /** @@ -104,6 +124,11 @@ interface IWorkbenchConstructionOptions { */ readonly resolveExternalUri?: IExternalUriResolver; + /** + * Support for creating tunnels. + */ + readonly tunnelFactory?: ITunnelFactory; + /** * Current logging level. Default is `LogLevel.Info`. */ diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index a049b4f2cd..fd4bdf21d4 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -60,25 +60,30 @@ import { BackupFileService } from 'vs/workbench/services/backup/common/backupFil import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagementService'; import { ITunnelService } from 'vs/platform/remote/common/tunnel'; -import { NoOpTunnelService } from 'vs/platform/remote/common/tunnelService'; +import { TunnelService } from 'vs/workbench/services/remote/common/tunnelService'; import { ILoggerService } from 'vs/platform/log/common/log'; import { FileLoggerService } from 'vs/platform/log/common/fileLogService'; -import { IAuthTokenService } from 'vs/platform/auth/common/auth'; -import { AuthTokenService } from 'vs/workbench/services/authToken/browser/authTokenService'; -import { IUserDataSyncStoreService, IUserDataSyncService, IUserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncStoreService, IUserDataSyncService, IUserDataSyncLogService, ISettingsSyncService, IUserDataAuthTokenService, IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; +import { AuthenticationService, IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; import { UserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSyncLog'; import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; +import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync'; +import { UserDataAuthTokenService } from 'vs/platform/userDataSync/common/userDataAuthTokenService'; +import { UserDataAutoSync } from 'vs/platform/userDataSync/common/userDataAutoSync'; registerSingleton(IExtensionManagementService, ExtensionManagementService); registerSingleton(IBackupFileService, BackupFileService); registerSingleton(IAccessibilityService, BrowserAccessibilityService, true); registerSingleton(IContextMenuService, ContextMenuService); -registerSingleton(ITunnelService, NoOpTunnelService, true); +registerSingleton(ITunnelService, TunnelService, true); registerSingleton(ILoggerService, FileLoggerService); -registerSingleton(IAuthTokenService, AuthTokenService); +registerSingleton(IAuthenticationService, AuthenticationService); registerSingleton(IUserDataSyncLogService, UserDataSyncLogService); registerSingleton(IUserDataSyncStoreService, UserDataSyncStoreService); +registerSingleton(IUserDataAuthTokenService, UserDataAuthTokenService); +registerSingleton(IUserDataAutoSyncService, UserDataAutoSync); +registerSingleton(ISettingsSyncService, SettingsSynchroniser); registerSingleton(IUserDataSyncService, UserDataSyncService); //#endregion @@ -89,6 +94,9 @@ registerSingleton(IUserDataSyncService, UserDataSyncService); // Explorer import 'vs/workbench/contrib/files/browser/files.web.contribution'; +// Backup +import 'vs/workbench/contrib/backup/browser/backup.web.contribution'; + // Preferences import 'vs/workbench/contrib/preferences/browser/keyboardLayoutPicker'; diff --git a/test/coverage.js b/test/coverage.js index f25c4956fb..53471093b4 100644 --- a/test/coverage.js +++ b/test/coverage.js @@ -17,11 +17,12 @@ const REPO_PATH = toUpperDriveLetter(path.join(__dirname, '..')); exports.initialize = function (loaderConfig) { const instrumenter = iLibInstrument.createInstrumenter(); loaderConfig.nodeInstrumenter = function (contents, source) { - if (minimatch(source, '**/test/**')) { // {{SQL CARBON EDIT}} Don't instrument test helper stuff either + if (minimatch(source, '**/test/**')) { + // tests don't get instrumented return contents; } // Try to find a .map file - let map = null; + let map = undefined; try { map = JSON.parse(fs.readFileSync(`${source}.map`).toString()); } catch (err) { @@ -34,34 +35,35 @@ exports.initialize = function (loaderConfig) { exports.createReport = function (isSingle) { const mapStore = iLibSourceMaps.createSourceMapStore(); const coverageMap = iLibCoverage.createCoverageMap(global.__coverage__); - const transformed = mapStore.transformCoverage(coverageMap); + return mapStore.transformCoverage(coverageMap).then((transformed) => { + // Paths come out all broken + let newData = Object.create(null); + Object.keys(transformed.data).forEach((file) => { + const entry = transformed.data[file]; + const fixedPath = fixPath(entry.path); + if (fixedPath.includes('\\vs\\') || fixedPath.includes('/vs/')) { return; } // {{SQL CARBON EDIT}} skip vscode files + entry.data.path = fixedPath; + newData[fixedPath] = entry; + }); + transformed.data = newData; - // Paths come out all broken - let newData = Object.create(null); - Object.keys(transformed.map.data).forEach((file) => { - const entry = transformed.map.data[file]; - const fixedPath = fixPath(entry.path); - if (fixedPath.includes('\\vs\\') || fixedPath.includes('/vs/')) { return; } // {{SQL CARBON EDIT}} skip vscode files - entry.data.path = fixedPath; - newData[fixedPath] = entry; + const context = iLibReport.createContext({ + dir: path.join(__dirname, `../.build/coverage${isSingle ? '-single' : ''}`), + coverageMap: transformed + }); + const tree = context.getTree('flat'); + + let reports = []; + if (isSingle) { + reports.push(iReports.create('lcovonly')); + } else { + reports.push(iReports.create('json')); + reports.push(iReports.create('lcov')); + reports.push(iReports.create('html')); + reports.push(iReports.create('cobertura')); // {{SQL CARBON EDIT}} add covertura + } + reports.forEach(report => tree.visit(report, context)); }); - transformed.map.data = newData; - - const tree = iLibReport.summarizers.flat(transformed.map); - const context = iLibReport.createContext({ - dir: path.join(__dirname, `../.build/coverage${isSingle ? '-single' : ''}`) - }); - - let reports = []; - if (isSingle) { - reports.push(iReports.create('lcovonly')); - } else { - reports.push(iReports.create('json')); - reports.push(iReports.create('lcov')); - reports.push(iReports.create('html')); - reports.push(iReports.create('cobertura')); // {{SQL CARBON EDIT}} add covertura - } - reports.forEach(report => tree.visit(report, context)); }; function toUpperDriveLetter(str) { diff --git a/test/electron/renderer.js b/test/electron/renderer.js index ccdfcd8fe6..6fe0d8ebd4 100644 --- a/test/electron/renderer.js +++ b/test/electron/renderer.js @@ -67,7 +67,7 @@ function initLoader(opts) { function createCoverageReport(opts) { if (opts.coverage) { - coverage.createReport(opts.run || opts.runGlob); + return coverage.createReport(opts.run || opts.runGlob); } return Promise.resolve(undefined); } diff --git a/tslint-gci.json b/tslint-gci.json deleted file mode 100644 index 981db69e57..0000000000 --- a/tslint-gci.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "extends": [ - "./tslint.json", - "./tslint-sql.json" - ], - "rules": { - "no-banned-terms": true, - "no-delete-expression": true, - "no-document-domain": true, - "no-disable-auto-sanitization": true, - "no-duplicate-parameter-names": true, - "no-exec-script": true, - "no-function-constructor-with-string-args": true, - "no-octal-literal": true, - "no-reserved-keywords": false, - "variable-name": { - "options": ["ban-keywords"] - }, - "no-string-based-set-immediate": true, - "no-string-based-set-interval": true, - "no-string-based-set-timeout": true, - "no-eval": true - }, - "defaultSeverity": "error", - "rulesDirectory": "./node_modules/tslint-microsoft-contrib/", - "linterOptions": { - "exclude": ["src/vs/**/*.ts"] - } -} \ No newline at end of file diff --git a/tslint-sql.json b/tslint-sql.json deleted file mode 100644 index 364885ef0c..0000000000 --- a/tslint-sql.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "rulesDirectory": [ - "build/lib/tslint" - ], - "rules": { - "double-quoted-string-arg": [ - true, - { - "signatures": [ - "localize", - "nls.localize" - ], - "argIndex": 1 - } - ], - "no-floating-promises": { - "severity": "warn" - }, - "await-promise": { - "options": [ - "Thenable", - "Deferred", - "PromiseLike" - ] - }, - "use-isnan": { - "severity": "error" - }, - "no-underscore-in-localize-keys": true - }, - "linterOptions": { - "exclude": [ - "src/vs/**/*.ts" - ] - } -} diff --git a/tslint.json b/tslint.json deleted file mode 100644 index f7bb25f3df..0000000000 --- a/tslint.json +++ /dev/null @@ -1,343 +0,0 @@ -{ - "rulesDirectory": [ - "build/lib/tslint" - ], - "rules": { - "no-arg": true, - "no-construct": true, - "no-debugger": true, - "no-duplicate-super": true, - "no-duplicate-switch-case": true, - "no-duplicate-variable": true, - "no-for-in-array": true, - "no-eval": true, - "no-redundant-jsdoc": true, - "no-restricted-globals": true, - "no-sparse-arrays": true, - "no-string-throw": true, - "no-unsafe-finally": true, - "no-unused-expression": true, - "no-var-keyword": true, - "number-literal-format": true, - "curly": true, - "class-name": true, - "label-position": true, - "semicolon": [ - true, - "always" - ], - "triple-equals": true, - "no-unexternalized-strings": [ - true, - { - "signatures": [ - "localize", - "nls.localize" - ], - "keyIndex": 0, - "messageIndex": 1 - } - ], - "layering": [ - true, - { - "common": [ - "browser" // {{SQL CARBON EDIT}} @anthonydresser not ideal, but for our purposes its fine for now - ], - "node": [ - "common" - ], - "browser": [ - "common" - ], - "electron-main": [ - "common", - "node" - ], - "electron-browser": [ - "common", - "browser", - "node" - ] - } - ], - // {{SQL CARBON EDIT}} @anthonydresser for the most part these rules should be the same as vscode with some changes - // anything that references **/vs/** should references **/{vs,sql}/** instead - // @angular/* and rxjs/* are added to browser and electron-browser restrictions since * doesn't cover them - // to begin with the only import patterns we are enforcing is import patterns in */base/* - "import-patterns": [ - true, - // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - // !!! Do not relax these rules !!! - // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - { - "target": "**/{vs,sql}/base/common/**", - "restrictions": [ - "vs/nls", - "**/{vs,sql}/base/common/**" - ] - }, - { - "target": "**/{vs,sql}/base/test/common/**", - "restrictions": [ - "assert", - "sinon", - "vs/nls", - "**/{vs,sql}/base/common/**", - "**/{vs,sql}/base/test/common/**" - ] - }, - { - "target": "**/{vs,sql}/base/browser/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/{vs,sql}/base/{common,browser}/**", - "@angular/*", - "rxjs/*" - ] - }, - { - "target": "**/{vs,sql}/base/node/**", - "restrictions": [ - "vs/nls", - "**/{vs,sql}/base/{common,browser,node}/**", - "!path" // node modules (except path where we have our own impl) - ] - }, - { - // vs/base/test/browser contains tests for vs/base/browser - "target": "**/{vs,sql}/base/test/browser/**", - "restrictions": [ - "assert", - "vs/nls", - "**/{vs,sql}/base/{common,browser}/**", - "**/{vs,sql}/base/test/{common,browser}/**", - "@angular/*", - "rxjs/*" - ] - }, - { - "target": "**/{vs,sql}/base/parts/*/common/**", - "restrictions": [ - "vs/nls", - "**/{vs,sql}/base/common/**", - "**/{vs,sql}/base/parts/*/common/**" - ] - }, - { - "target": "**/{vs,sql}/base/parts/*/browser/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/{vs,sql}/base/{common,browser}/**", - "**/{vs,sql}/base/parts/*/{common,browser}/**", - "@angular/*", - "rxjs/*" - ] - }, - { - "target": "**/{vs,sql}/base/parts/*/node/**", - "restrictions": [ - "vs/nls", - "**/{vs,sql}/base/{common,browser,node}/**", - "**/{vs,sql}/base/parts/*/{common,browser,node}/**", - "!path" // node modules (except path where we have our own impl) - ] - }, - { - "target": "**/{vs,sql}/base/parts/*/electron-browser/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/{vs,sql}/base/{common,browser,node,electron-browser}/**", - "**/{vs,sql}/base/parts/*/{common,browser,node,electron-browser}/**", - "!path", // node modules (except path where we have our own impl) - "@angular/*", - "rxjs/*" - ] - }, - { - "target": "**/{vs,sql}/base/parts/*/electron-main/**", - "restrictions": [ - "vs/nls", - "**/{vs,sql}/base/{common,browser,node,electron-main}/**", - "**/{vs,sql}/base/parts/*/{common,browser,node,electron-main}/**", - "!path", // node modules (except path where we have our own impl) - "@angular/*", - "rxjs/*" - ] - }, - { - "target": "**/{vs,sql}/platform/*/common/**", - "restrictions": [ - "vs/nls", - "**/{vs,sql}/base/common/**", - "**/{vs,sql}/base/parts/*/common/**", - "**/{vs,sql}/{platform,workbench}/**/common/**", - "**/vs/editor/common/**", - "azdata" // TODO remove - ] - }, - { - "target": "**/{vs,sql}/platform/*/test/common/**", - "restrictions": [ - "assert", - "sinon", - "vs/nls", - "**/{vs,sql}/base/common/**", - "**/{vs,sql}/{platform,workbench}/**/common/**", - "**/{vs,sql}/{base,platform,workbench}/**/test/common/**", - "typemoq", - "azdata" // TODO remove - ] - }, - { - "target": "**/{vs,sql}/platform/*/browser/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/{vs,sql}/base/{common,browser}/**", - "**/{vs,sql}/base/parts/*/{common,browser}/**", - "**/{vs,sql}/{platform,workbench}/**/{common,browser}/**", - "**/vs/editor/{common,browser}/**", - "@angular/*", - "rxjs/*", - "azdata" // TODO remove - ] - }, - { - "target": "**/{vs,sql}/platform/*/node/**", - "restrictions": [ - "vs/nls", - "**/{vs,sql}/base/{common,browser,node}/**", - "**/{vs,sql}/base/parts/*/{common,browser,node}/**", - "**/{vs,sql}/{platform,workbench}/**/{common,browser,node}/**", - "**/vs/editor/{common,browser,node}/**", - "!path", // node modules (except path where we have our own impl) - "azdata" // TODO remove - ] - }, - { - "target": "**/{vs,sql}/platform/*/electron-browser/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/{vs,sql}/base/{common,browser,node}/**", - "**/{vs,sql}/base/parts/*/{common,browser,node,electron-browser}/**", - "**/{vs,sql}/{platform,workbench}/**/{common,browser,node,electron-browser}/**", - "**/vs/editor/{common,browser,node,electron-browser}/**", - "!path", // node modules (except path where we have our own impl) - "@angular/*", - "rxjs/*", - "azdata" // TODO remove - ] - }, - { - "target": "**/{vs,sql}/platform/*/electron-main/**", - "restrictions": [ - "vs/nls", - "**/{vs,sql}/base/{common,browser,node}/**", - "**/{vs,sql}/base/parts/*/{common,browser,node,electron-browser}/**", - "**/{vs,sql}/{platform,workbench}/**/{common,browser,node,electron-main}/**", - "**/vs/editor/{common,browser,node,electron-main}/**", - "!path", // node modules (except path where we have our own impl) - "azdata" // TODO remove - ] - }, - { - "target": "**/{vs,sql}/platform/*/test/browser/**", - "restrictions": [ - "assert", - "sinon", - "vs/nls", - "**/{vs,sql}/base/{common,browser}/**", - "**/{vs,sql}/{platform,workbench}/**/{common,browser}/**", - "**/{vs,sql}/{base,platform,workbench}/**/test/{common,browser}/**", - "typemoq", - "@angular/*", - "rxjs/*", - "azdata" // TODO remove - ] - } - ], - // {{SQL CARBON EDIT}} - // remove import patterns and layering - "no-nodejs-globals": [ - true, - { - "target": "**/{vs,sql}/base/common/{path,process,platform}.ts", - "allowed": [ - "process" // -> defines safe access to process - ] - }, - { - "target": "**/{vs,sql}/**/test/{common,browser}/**", - "allowed": [ - "process", - "Buffer", - "__filename", - "__dirname" - ] - }, - { - "target": "**/{vs,sql}/workbench/api/common/extHostExtensionService.ts", - "allowed": [ - "global" // -> safe access to 'global' - ] - }, - { - "target": "**/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts", - "allowed": [ - "process" - ] - }, - { - "target": "**/{vs,sql}/**/{common,browser}/**", - "allowed": [ /* none */] - } - ], - "no-dom-globals": [ - true, - { - "target": "**/{vs,sql}/**/test/{common,node,electron-main}/**", - "allowed": [ - "document", - "HTMLElement" - ] - }, - { - "target": "**/vs/workbench/contrib/terminal/common/{terminal.ts,terminalService.ts}", - "allowed": [ - "HTMLElement" - ] - }, - { - "target": "**/{vs,sql}/**/{common,node,electron-main}/**", - "allowed": [ /* none */] - } - ], - "duplicate-imports": true, - "no-new-buffer": true, - "translation-remind": true, - "no-standalone-editor": true, - "no-nls-in-standalone-editor": true, - "no-sync": [ - true, - { - "exclude": [ - "**/vs/**", // assume they are doing the right thing - "**/extensions/git/**", // assume they are doing the right thing, - "**/extensions/extension-editing/**", // assume they are doing the right thing, - "**/json-language-features/**", // assume they are doing the right thing, - "**/vscode-test-resolver/**", // testing doesn't matter - "**/integration-tests/**", // testing doesn't matter - "**/*.test.*", // testing doesn't matter - "**/test/**" // testing doesn't matter - ] - } - ], - "no-useless-strict": true - }, - "defaultSeverity": "warning" -} diff --git a/yarn.lock b/yarn.lock index 924e219f0f..06901f2bc2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -54,39 +54,75 @@ dependencies: "@babel/highlight" "^7.0.0" -"@babel/generator@^7.4.0", "@babel/generator@^7.5.0": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.5.0.tgz#f20e4b7a91750ee8b63656073d843d2a736dca4a" - integrity sha512-1TTVrt7J9rcG5PMjvO7VEG3FrEoEJNHxumRq66GemPmzboLWtIjjcJgk8rokuAS7IiRSpgVSu5Vb9lc99iJkOA== +"@babel/code-frame@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" + integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g== dependencies: - "@babel/types" "^7.5.0" - jsesc "^2.5.1" - lodash "^4.17.11" + "@babel/highlight" "^7.8.3" + +"@babel/core@^7.7.5": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.8.3.tgz#30b0ebb4dd1585de6923a0b4d179e0b9f5d82941" + integrity sha512-4XFkf8AwyrEG7Ziu3L2L0Cv+WyY47Tcsp70JFmpftbAA1K7YL/sgE9jh9HyNj08Y/U50ItUchpN0w6HxAoX1rA== + dependencies: + "@babel/code-frame" "^7.8.3" + "@babel/generator" "^7.8.3" + "@babel/helpers" "^7.8.3" + "@babel/parser" "^7.8.3" + "@babel/template" "^7.8.3" + "@babel/traverse" "^7.8.3" + "@babel/types" "^7.8.3" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.1" + json5 "^2.1.0" + lodash "^4.17.13" + resolve "^1.3.2" + semver "^5.4.1" source-map "^0.5.0" - trim-right "^1.0.1" -"@babel/helper-function-name@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz#a0ceb01685f73355d4360c1247f582bfafc8ff53" - integrity sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw== +"@babel/generator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.8.3.tgz#0e22c005b0a94c1c74eafe19ef78ce53a4d45c03" + integrity sha512-WjoPk8hRpDRqqzRpvaR8/gDUPkrnOOeuT2m8cNICJtZH6mwaCo3v0OKMI7Y6SM1pBtyijnLtAL0HDi41pf41ug== dependencies: - "@babel/helper-get-function-arity" "^7.0.0" - "@babel/template" "^7.1.0" - "@babel/types" "^7.0.0" + "@babel/types" "^7.8.3" + jsesc "^2.5.1" + lodash "^4.17.13" + source-map "^0.5.0" -"@babel/helper-get-function-arity@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz#83572d4320e2a4657263734113c42868b64e49c3" - integrity sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ== +"@babel/helper-function-name@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz#eeeb665a01b1f11068e9fb86ad56a1cb1a824cca" + integrity sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA== dependencies: - "@babel/types" "^7.0.0" + "@babel/helper-get-function-arity" "^7.8.3" + "@babel/template" "^7.8.3" + "@babel/types" "^7.8.3" -"@babel/helper-split-export-declaration@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz#ff94894a340be78f53f06af038b205c49d993677" - integrity sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q== +"@babel/helper-get-function-arity@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz#b894b947bd004381ce63ea1db9f08547e920abd5" + integrity sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA== dependencies: - "@babel/types" "^7.4.4" + "@babel/types" "^7.8.3" + +"@babel/helper-split-export-declaration@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz#31a9f30070f91368a7182cf05f831781065fc7a9" + integrity sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA== + dependencies: + "@babel/types" "^7.8.3" + +"@babel/helpers@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.8.3.tgz#382fbb0382ce7c4ce905945ab9641d688336ce85" + integrity sha512-LmU3q9Pah/XyZU89QvBgGt+BCsTPoQa+73RxAQh8fb8qkDyIfeQnmgs+hvzhTCKTzqOyk7JTkS3MS1S8Mq5yrQ== + dependencies: + "@babel/template" "^7.8.3" + "@babel/traverse" "^7.8.3" + "@babel/types" "^7.8.3" "@babel/highlight@^7.0.0": version "7.0.0" @@ -97,44 +133,85 @@ esutils "^2.0.2" js-tokens "^4.0.0" -"@babel/parser@^7.4.3", "@babel/parser@^7.4.4", "@babel/parser@^7.5.0": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.5.0.tgz#3e0713dff89ad6ae37faec3b29dcfc5c979770b7" - integrity sha512-I5nW8AhGpOXGCCNYGc+p7ExQIBxRFnS2fd/d862bNOKvmoEPjYPcfIjsfdy0ujagYOIYPczKgD9l3FsgTkAzKA== - -"@babel/template@^7.1.0", "@babel/template@^7.4.0": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237" - integrity sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw== +"@babel/highlight@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.8.3.tgz#28f173d04223eaaa59bc1d439a3836e6d1265797" + integrity sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg== dependencies: - "@babel/code-frame" "^7.0.0" - "@babel/parser" "^7.4.4" - "@babel/types" "^7.4.4" + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^4.0.0" -"@babel/traverse@^7.4.3": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.5.0.tgz#4216d6586854ef5c3c4592dab56ec7eb78485485" - integrity sha512-SnA9aLbyOCcnnbQEGwdfBggnc142h/rbqqsXcaATj2hZcegCl903pUD/lfpsNBlBSuWow/YDfRyJuWi2EPR5cg== +"@babel/parser@^7.7.5", "@babel/parser@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.8.3.tgz#790874091d2001c9be6ec426c2eed47bc7679081" + integrity sha512-/V72F4Yp/qmHaTALizEm9Gf2eQHV3QyTL3K0cNfijwnMnb1L+LDlAubb/ZnSdGAVzVSWakujHYs1I26x66sMeQ== + +"@babel/template@^7.7.4", "@babel/template@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.3.tgz#e02ad04fe262a657809327f578056ca15fd4d1b8" + integrity sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ== dependencies: - "@babel/code-frame" "^7.0.0" - "@babel/generator" "^7.5.0" - "@babel/helper-function-name" "^7.1.0" - "@babel/helper-split-export-declaration" "^7.4.4" - "@babel/parser" "^7.5.0" - "@babel/types" "^7.5.0" + "@babel/code-frame" "^7.8.3" + "@babel/parser" "^7.8.3" + "@babel/types" "^7.8.3" + +"@babel/traverse@^7.7.4", "@babel/traverse@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.8.3.tgz#a826215b011c9b4f73f3a893afbc05151358bf9a" + integrity sha512-we+a2lti+eEImHmEXp7bM9cTxGzxPmBiVJlLVD+FuuQMeeO7RaDbutbgeheDkw+Xe3mCfJHnGOWLswT74m2IPg== + dependencies: + "@babel/code-frame" "^7.8.3" + "@babel/generator" "^7.8.3" + "@babel/helper-function-name" "^7.8.3" + "@babel/helper-split-export-declaration" "^7.8.3" + "@babel/parser" "^7.8.3" + "@babel/types" "^7.8.3" debug "^4.1.0" globals "^11.1.0" - lodash "^4.17.11" + lodash "^4.17.13" -"@babel/types@^7.0.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.5.0": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.5.0.tgz#e47d43840c2e7f9105bc4d3a2c371b4d0c7832ab" - integrity sha512-UFpDVqRABKsW01bvw7/wSUe56uy6RXM5+VJibVVAybDGxEW25jdwiFJEf7ASvSaC7sN7rbE/l3cLp2izav+CtQ== +"@babel/types@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.3.tgz#5a383dffa5416db1b73dedffd311ffd0788fb31c" + integrity sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg== dependencies: esutils "^2.0.2" - lodash "^4.17.11" + lodash "^4.17.13" to-fast-properties "^2.0.0" +"@electron/get@^1.0.1": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@electron/get/-/get-1.7.2.tgz#286436a9fb56ff1a1fcdf0e80131fd65f4d1e0fd" + integrity sha512-LSE4LZGMjGS9TloDx0yO44D2UTbaeKRk+QjlhWLiQlikV6J4spgDCjb6z4YIcqmPAwNzlNCnWF4dubytwI+ATA== + dependencies: + debug "^4.1.1" + env-paths "^2.2.0" + fs-extra "^8.1.0" + got "^9.6.0" + sanitize-filename "^1.6.2" + sumchecker "^3.0.1" + optionalDependencies: + global-agent "^2.0.2" + global-tunnel-ng "^2.7.1" + +"@istanbuljs/schema@^0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" + integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== + +"@sindresorhus/is@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" + integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== + +"@szmarczak/http-timer@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" + integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== + dependencies: + defer-to-connect "^1.0.1" + "@types/applicationinsights@0.20.0": version "0.20.0" resolved "https://registry.yarnpkg.com/@types/applicationinsights/-/applicationinsights-0.20.0.tgz#fa7b36dc954f635fa9037cad27c378446b1048fb" @@ -171,10 +248,10 @@ resolved "https://registry.yarnpkg.com/@types/d3/-/d3-3.5.42.tgz#6a782b44bb7f5c48165cb166886b5d53cb84455f" integrity sha512-jKnkXluwSAzkvR19zjCHvLYgsWuDqpeE79NrhWrqhKqrx3sgTRqqt4SKaxSy+N7mt1J04Xy4L0/cKdfIgnjzVQ== -"@types/fancy-log@1.3.0": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@types/fancy-log/-/fancy-log-1.3.0.tgz#a61ab476e5e628cd07a846330df53b85e05c8ce0" - integrity sha512-mQjDxyOM1Cpocd+vm1kZBP7smwKZ4TNokFeds9LV7OZibmPJFEzY3+xZMrKfUdNT71lv8GoCPD6upKwHxubClw== +"@types/eslint-visitor-keys@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" + integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== "@types/graceful-fs@4.1.2": version "4.1.2" @@ -204,6 +281,11 @@ dependencies: "@types/node" "*" +"@types/json-schema@^7.0.3": + version "7.0.4" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339" + integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA== + "@types/keytar@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@types/keytar/-/keytar-4.4.0.tgz#ca24e6ee6d0df10c003aafe26e93113b8faf0d8e" @@ -224,10 +306,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.21.tgz#7e8a0c34cf29f4e17a36e9bd0ea72d45ba03908e" integrity sha512-CBgLNk4o3XMnqMc0rhb6lc77IwShMEglz05deDcn2lQxyXEZivfwgYJu7SMha9V5XcrP6qZuevTHV/QrN2vjKQ== -"@types/node@^10.12.18": - version "10.17.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.9.tgz#4f251a1ed77ac7ef09d456247d67fc8173f6b9da" - integrity sha512-+6VygF9LbG7Gaqeog2G7u1+RUcmo0q1rI+2ZxdIg2fAUngk5Vz9fOCHXdloNUOHEPd1EuuOpL5O0CdgN9Fx5UQ== +"@types/node@^12.0.12": + version "12.12.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.24.tgz#d4606afd8cf6c609036b854360367d1b2c78931f" + integrity sha512-1Ciqv9pqwVtW6FsIUKSZNB82E5Cu1I2bBTj1xuIHXLe/1zYLl3956Nbhg2MzSYHVfl9/rmanjbQIb7LibfCnug== "@types/node@^12.11.7": version "12.12.14" @@ -319,6 +401,68 @@ dependencies: "@types/node" "*" +"@typescript-eslint/eslint-plugin@2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.3.2.tgz#7e112ca0bb29044d915baf10163a8199a20f7c69" + integrity sha512-tcnpksq1bXzcIRbYLeXkgp6l+ggEMXXUcl1wsSvL807fRtmvVQKygElwEUf4hBA76dNag3VAK1q2m3vd7qJaZA== + dependencies: + "@typescript-eslint/experimental-utils" "2.3.2" + eslint-utils "^1.4.2" + functional-red-black-tree "^1.0.1" + regexpp "^2.0.1" + tsutils "^3.17.1" + +"@typescript-eslint/experimental-utils@2.13.0": + version "2.13.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.13.0.tgz#958614faa6f77599ee2b241740e0ea402482533d" + integrity sha512-+Hss3clwa6aNiC8ZjA45wEm4FutDV5HsVXPl/rDug1THq6gEtOYRGLqS3JlTk7mSnL5TbJz0LpEbzbPnKvY6sw== + dependencies: + "@types/json-schema" "^7.0.3" + "@typescript-eslint/typescript-estree" "2.13.0" + eslint-scope "^5.0.0" + +"@typescript-eslint/experimental-utils@2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.3.2.tgz#e50f31264507e6fec7b33840bb6af260c24f4ea8" + integrity sha512-t+JGdTT6dRbmvKDlhlVkEueoZa0fhJNfG6z2cpnRPLwm3VwYr2BjR//acJGC1Yza0I9ZNcDfRY7ubQEvvfG6Jg== + dependencies: + "@types/json-schema" "^7.0.3" + "@typescript-eslint/typescript-estree" "2.3.2" + eslint-scope "^5.0.0" + +"@typescript-eslint/parser@^2.12.0": + version "2.13.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.13.0.tgz#ea1ab394cf9ca17467e3da7f96eca9309f57c326" + integrity sha512-vbDeLr5QRJ1K7x5iRK8J9wuGwR9OVyd1zDAY9XFAQvAosHVjSVbDgkm328ayE6hx2QWVGhwvGaEhedcqAbfQcA== + dependencies: + "@types/eslint-visitor-keys" "^1.0.0" + "@typescript-eslint/experimental-utils" "2.13.0" + "@typescript-eslint/typescript-estree" "2.13.0" + eslint-visitor-keys "^1.1.0" + +"@typescript-eslint/typescript-estree@2.13.0": + version "2.13.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.13.0.tgz#a2e746867da772c857c13853219fced10d2566bc" + integrity sha512-t21Mg5cc8T3ADEUGwDisHLIubgXKjuNRbkpzDMLb7/JMmgCe/gHM9FaaujokLey+gwTuLF5ndSQ7/EfQqrQx4g== + dependencies: + debug "^4.1.1" + eslint-visitor-keys "^1.1.0" + glob "^7.1.6" + is-glob "^4.0.1" + lodash.unescape "4.0.1" + semver "^6.3.0" + tsutils "^3.17.1" + +"@typescript-eslint/typescript-estree@2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.3.2.tgz#107414aa04e689fe6f7251eb63fb500217f2b7f4" + integrity sha512-eZNEAai16nwyhIVIEaWQlaUgAU3S9CkQ58qvK0+3IuSdLJD3W1PNuehQFMIhW/mTP1oFR9GNoTcLg7gtXz6lzA== + dependencies: + glob "^7.1.4" + is-glob "^4.0.1" + lodash.unescape "4.0.1" + semver "^6.3.0" + "@webassemblyjs/ast@1.5.13": version "1.5.13" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.5.13.tgz#81155a570bd5803a30ec31436bc2c9c0ede38f25" @@ -498,6 +642,11 @@ acorn-jsx@^5.0.0: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.1.tgz#32a064fd925429216a09b141102bfdd185fae40e" integrity sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg== +acorn-jsx@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.1.0.tgz#294adb71b57398b0680015f0a38c563ee1db5384" + integrity sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw== + acorn@^5.0.0, acorn@^5.6.2: version "5.7.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.1.tgz#f095829297706a7c9776958c0afc8930a9b9d9d8" @@ -508,6 +657,11 @@ acorn@^6.0.2: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.0.7.tgz#490180ce18337270232d9488a44be83d9afb7fd3" integrity sha512-HNJNgE60C9eOTgn974Tlp3dpLZdUr+SoxxDwPaY9J/kDNOLQTkaDgwBUXAF4SSsrAwD9RpdxuHK/EbuF+W9Ahw== +acorn@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.0.tgz#949d36f2c292535da602283586c2477c57eb2d6c" + integrity sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ== + agent-base@4: version "4.2.0" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.0.tgz#9838b5c3392b962bad031e6a4c5e1024abec45ce" @@ -564,6 +718,16 @@ ajv@^6.1.0: json-schema-traverse "^0.4.1" uri-js "^4.2.1" +ajv@^6.10.0, ajv@^6.10.2: + version "6.10.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" + integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw== + dependencies: + fast-deep-equal "^2.0.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + ajv@^6.5.3, ajv@^6.6.1: version "6.8.1" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.8.1.tgz#0890b93742985ebf8973cd365c5b23920ce3cb20" @@ -617,6 +781,13 @@ ansi-escapes@^3.2.0: resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== +ansi-escapes@^4.2.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.0.tgz#a4ce2b33d6b214b7950d8595c212f12ac9cc569d" + integrity sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg== + dependencies: + type-fest "^0.8.1" + ansi-gray@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/ansi-gray/-/ansi-gray-0.1.1.tgz#2962cf54ec9792c48510a3deb524436861ef7251" @@ -651,6 +822,11 @@ ansi-regex@^4.1.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" @@ -811,11 +987,6 @@ array-each@^1.0.0, array-each@^1.0.1: resolved "https://registry.yarnpkg.com/array-each/-/array-each-1.0.1.tgz#a794af0c05ab1752846ee753a1f211a05ba0c44f" integrity sha1-p5SvDAWrF1KEbudTofIRoFugxE8= -array-find-index@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" - integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= - array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" @@ -1186,6 +1357,11 @@ boolbase@~1.0.0: resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= +boolean@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.0.0.tgz#fab78d5907dbae6216ab46d32733bb7b76b99e76" + integrity sha512-OElxJ1lUSinuoUnkpOgLmxp0DC4ytEhODEL6QJU0NpxE/mI4rUSh8h1P1Wkvfi3xQEBcxXR2gBIPNYNuaFcAbQ== + boom@2.x.x: version "2.10.1" resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" @@ -1381,7 +1557,7 @@ buffers@~0.1.1: resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" integrity sha1-skV5w77U1tOWru5tmorn9Ugqt7s= -builtin-modules@^1.0.0, builtin-modules@^1.1.1: +builtin-modules@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= @@ -1430,24 +1606,24 @@ cache-base@^1.0.1: union-value "^1.0.0" unset-value "^1.0.0" +cacheable-request@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" + integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^3.0.0" + lowercase-keys "^2.0.0" + normalize-url "^4.1.0" + responselike "^1.0.2" + callsites@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.0.0.tgz#fb7eb569b72ad7a45812f93fd9430a3e410b3dd3" integrity sha512-tWnkwu9YEq2uzlBDI4RcLn8jrFvF9AOi8PxDNU3hZZjJcjkcRAq3vCI+vZcg1SuxISDYe86k9VZFwAxDiJGoAw== -camelcase-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" - integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc= - dependencies: - camelcase "^2.0.0" - map-obj "^1.0.0" - -camelcase@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" - integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= - camelcase@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" @@ -1490,15 +1666,6 @@ chainsaw@~0.1.0: dependencies: traverse ">=0.3.0 <0.4" -chalk@2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.1.tgz#523fe2678aec7b04e8041909292fe8b17059b796" - integrity sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g== - dependencies: - ansi-styles "^3.2.0" - escape-string-regexp "^1.0.5" - supports-color "^5.2.0" - chalk@2.4.2, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -1703,6 +1870,13 @@ cli-cursor@^2.1.0: dependencies: restore-cursor "^2.0.0" +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + cli-width@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" @@ -1731,6 +1905,13 @@ clone-buffer@^1.0.0: resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg= +clone-response@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" + integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= + dependencies: + mimic-response "^1.0.0" + clone-stats@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1" @@ -1906,26 +2087,26 @@ commander@2.3.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.3.0.tgz#fd430e889832ec353b9acd1de217c11cb3eef873" integrity sha1-/UMOiJgy7DU7ms0d4hfBHLPu+HM= -commander@^2.12.1, commander@~2.13.0: - version "2.13.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" - integrity sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA== - commander@^2.19.0: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== -commander@~2.20.0: - version "2.20.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" - integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== +commander@~2.13.0: + version "2.13.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" + integrity sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA== commandpost@^1.0.0: version "1.2.1" resolved "https://registry.yarnpkg.com/commandpost/-/commandpost-1.2.1.tgz#2e9c4c7508b9dc704afefaa91cab92ee6054cc68" integrity sha512-V1wzc+DTFsO96te2W/U+fKNRSOWtOwXhkkZH2WRLLbucrY+YrDNsRr4vtfSf83MUZVF3E6B4nwT30fqaTpzipQ== +comment-parser@^0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-0.7.2.tgz#baf6d99b42038678b81096f15b630d18142f4b8a" + integrity sha512-4Rjb1FnxtOcv9qsfuaNuVsmmVn4ooVoBHzYfyKteiXwIU84PClyGA5jASoFMwPV93+FPh9spwueXauxFJZkGAg== + commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -1958,7 +2139,7 @@ concat-with-sourcemaps@^1.0.0: dependencies: source-map "^0.5.1" -config-chain@^1.1.12: +config-chain@^1.1.11, config-chain@^1.1.12: version "1.1.12" resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa" integrity sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA== @@ -2013,6 +2194,13 @@ convert-source-map@^1.5.0: dependencies: safe-buffer "~5.1.1" +convert-source-map@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" + integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== + dependencies: + safe-buffer "~5.1.1" + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -2062,6 +2250,11 @@ copy-webpack-plugin@^4.5.2: p-limit "^1.0.0" serialize-javascript "^1.4.0" +core-js@^3.4.1: + version "3.6.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.1.tgz#39d5e2e346258cc01eb7d44345b1c3c014ca3f05" + integrity sha512-186WjSik2iTGfDjfdCZAxv2ormxtKgemjC3SI6PL31qOA0j5LhTDVjHChccoc7brwLvpvLPiMyRlcO88C4l1QQ== + core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -2246,13 +2439,6 @@ cuint@^0.2.1: resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b" integrity sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs= -currently-unhandled@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" - integrity sha1-mI3zP+qxke95mmE2nddsF635V+o= - dependencies: - array-find-index "^1.0.1" - cyclist@~0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" @@ -2289,7 +2475,7 @@ debug@2.2.0: dependencies: ms "0.7.1" -debug@2.6.9, debug@^2.1.2, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3: +debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -2303,7 +2489,7 @@ debug@3.1.0: dependencies: ms "2.0.0" -debug@^3.0.0, debug@^3.1.0: +debug@^3.1.0: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== @@ -2374,7 +2560,12 @@ default-resolution@^2.0.0: resolved "https://registry.yarnpkg.com/default-resolution/-/default-resolution-2.0.0.tgz#bcb82baa72ad79b426a76732f1a81ad6df26d684" integrity sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ= -define-properties@^1.1.2: +defer-to-connect@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.1.tgz#88ae694b93f67b81815a2c8c769aef6574ac8f2f" + integrity sha512-J7thop4u3mRTkYRQ+Vpfwy2G5Ehoy82I14+14W4YMDLKdWloI9gSzRbV30s/NckQGVJtPkWNcW4oMAUigTdqiQ== + +define-properties@^1.1.2, define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== @@ -2474,6 +2665,11 @@ detect-libc@^1.0.2, detect-libc@^1.0.3: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= +detect-node@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" + integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== + diagnostic-channel-publishers@0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.2.1.tgz#8e2d607a8b6d79fe880b548bc58cc6beb288c4f3" @@ -2496,11 +2692,6 @@ diff@1.4.0: resolved "https://registry.yarnpkg.com/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf" integrity sha1-fyjS657nsVqX79ic5j3P2qPMur8= -diff@^3.2.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c" - integrity sha512-QpVuMTEoJMF7cKzi6bvWhRulU1fZqZnvyVQgNhPaxxuTYwyjn/j1v9falseQ/uXWwPnO56RBfwtg4h/EQXmucA== - diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" @@ -2525,6 +2716,13 @@ doctrine@^2.1.0: dependencies: esutils "^2.0.2" +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + dom-serializer@0, dom-serializer@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" @@ -2576,6 +2774,11 @@ domutils@^1.5.1: dom-serializer "0" domelementtype "1" +duplexer3@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" + integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= + duplexer@^0.1.1, duplexer@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" @@ -2645,33 +2848,18 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -electron-download@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/electron-download/-/electron-download-4.1.1.tgz#02e69556705cc456e520f9e035556ed5a015ebe8" - integrity sha512-FjEWG9Jb/ppK/2zToP+U5dds114fM1ZOJqMAR4aXXL5CvyPE9fiqBK/9YcwC9poIFQTEJk/EM/zyRwziziRZrg== - dependencies: - debug "^3.0.0" - env-paths "^1.0.0" - fs-extra "^4.0.1" - minimist "^1.2.0" - nugget "^2.0.1" - path-exists "^3.0.0" - rc "^1.2.1" - semver "^5.4.1" - sumchecker "^2.0.2" - electron-to-chromium@^1.2.7: version "1.3.27" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.27.tgz#78ecb8a399066187bb374eede35d9c70565a803d" integrity sha1-eOy4o5kGYYe7N07t412ccFZagD0= -electron@6.1.6: - version "6.1.6" - resolved "https://registry.yarnpkg.com/electron/-/electron-6.1.6.tgz#d63ea9c89b85b981a29eb3088bf391bf52bd8d73" - integrity sha512-4c4GiFTbWY2Mgv20HB4Bfhf1hDKb0MWgC35wkwNepNom1ioWfumocPHZrSs1xNAEe+tOmezY6lq74n3LbwTnVQ== +electron@7.1.7: + version "7.1.7" + resolved "https://registry.yarnpkg.com/electron/-/electron-7.1.7.tgz#520e2bc422e3dfd4bae166dd3be62101f2cbdc52" + integrity sha512-aCLJ4BJwnvOckJgovNul22AYlMFDzm4S4KqKCG2iBlFJyMHBxXAKFKMsgYd40LBZWS3hcY6RHpaYjHSAPLS1pw== dependencies: - "@types/node" "^10.12.18" - electron-download "^4.1.0" + "@electron/get" "^1.0.1" + "@types/node" "^12.0.12" extract-zip "^1.0.3" elliptic@^6.0.0: @@ -2699,11 +2887,21 @@ emoji-regex@^7.0.1: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + emojis-list@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k= +encodeurl@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + encodeurl@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" @@ -2730,10 +2928,10 @@ entities@^1.1.1, entities@~1.1.1: resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" integrity sha1-blwtClYhtdra7O+AuQ7ftc13cvA= -env-paths@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-1.0.0.tgz#4168133b42bb05c38a35b1ae4397c8298ab369e0" - integrity sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA= +env-paths@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43" + integrity sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA== errno@^0.1.3, errno@~0.1.7: version "0.1.7" @@ -2757,6 +2955,11 @@ es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14: es6-iterator "~2.0.1" es6-symbol "~3.1.1" +es6-error@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" + integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== + es6-iterator@^2.0.1, es6-iterator@~2.0.1: version "2.0.3" resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" @@ -2811,6 +3014,25 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.3, escape-string-regexp@^ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + +eslint-plugin-jsdoc@^19.1.0: + version "19.1.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-19.1.0.tgz#fcc17f0378fdd6ee1c847a79b7211745cb05d014" + integrity sha512-rw8ouveUzz41dgSCyjlZgh5cKuQIyBzsrJnKeGYoY74+9AXuOygAQMwvyN4bMRp0hJu0DYQptKyQiSBqOnXmTg== + dependencies: + comment-parser "^0.7.2" + debug "^4.1.1" + jsdoctypeparser "^6.1.0" + lodash "^4.17.15" + object.entries-ponyfill "^1.0.1" + regextras "^0.7.0" + semver "^6.3.0" + spdx-expression-parse "^3.0.0" + eslint-scope@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.0.tgz#50bf3071e9338bcdc43331794a0cb533f0136172" @@ -2819,16 +3041,79 @@ eslint-scope@^4.0.0: esrecurse "^4.1.0" estraverse "^4.1.1" +eslint-scope@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9" + integrity sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw== + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + eslint-utils@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.3.1.tgz#9a851ba89ee7c460346f97cf8939c7298827e512" integrity sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q== +eslint-utils@^1.4.2, eslint-utils@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" + integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== + dependencies: + eslint-visitor-keys "^1.1.0" + eslint-visitor-keys@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" integrity sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ== +eslint-visitor-keys@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" + integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== + +eslint@6.8.0: + version "6.8.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb" + integrity sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig== + dependencies: + "@babel/code-frame" "^7.0.0" + ajv "^6.10.0" + chalk "^2.1.0" + cross-spawn "^6.0.5" + debug "^4.0.1" + doctrine "^3.0.0" + eslint-scope "^5.0.0" + eslint-utils "^1.4.3" + eslint-visitor-keys "^1.1.0" + espree "^6.1.2" + esquery "^1.0.1" + esutils "^2.0.2" + file-entry-cache "^5.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^5.0.0" + globals "^12.1.0" + ignore "^4.0.6" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + inquirer "^7.0.0" + is-glob "^4.0.0" + js-yaml "^3.13.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.3.0" + lodash "^4.17.14" + minimatch "^3.0.4" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + optionator "^0.8.3" + progress "^2.0.0" + regexpp "^2.0.1" + semver "^6.1.2" + strip-ansi "^5.2.0" + strip-json-comments "^3.0.1" + table "^5.2.3" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + eslint@^5.0.1: version "5.13.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.13.0.tgz#ce71cc529c450eed9504530939aa97527861ede9" @@ -2880,6 +3165,15 @@ espree@^5.0.0: acorn-jsx "^5.0.0" eslint-visitor-keys "^1.0.0" +espree@^6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/espree/-/espree-6.1.2.tgz#6c272650932b4f91c3714e5e7b5f5e2ecf47262d" + integrity sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA== + dependencies: + acorn "^7.1.0" + acorn-jsx "^5.1.0" + eslint-visitor-keys "^1.1.0" + esprima@^2.6.0: version "2.7.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" @@ -3124,7 +3418,7 @@ extsprintf@1.3.0, extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= -fancy-log@1.3.2, fancy-log@^1.3.2: +fancy-log@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.2.tgz#f41125e3d84f2e7d89a43d06d958c8f78be16be1" integrity sha1-9BEl49hPLn2JpD0G2VjI94vha+E= @@ -3158,7 +3452,7 @@ fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= -fast-levenshtein@~2.0.4: +fast-levenshtein@~2.0.4, fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= @@ -3189,6 +3483,13 @@ figures@^2.0.0: dependencies: escape-string-regexp "^1.0.5" +figures@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.1.0.tgz#4b198dd07d8d71530642864af2d45dd9e459c4ec" + integrity sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg== + dependencies: + escape-string-regexp "^1.0.5" + file-entry-cache@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" @@ -3197,6 +3498,13 @@ file-entry-cache@^2.0.0: flat-cache "^1.2.1" object-assign "^4.0.1" +file-entry-cache@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" + integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== + dependencies: + flat-cache "^2.0.1" + 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" @@ -3335,6 +3643,20 @@ flat-cache@^1.2.1: graceful-fs "^4.1.2" write "^0.2.1" +flat-cache@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" + integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== + dependencies: + flatted "^2.0.0" + rimraf "2.6.3" + write "1.0.3" + +flatted@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08" + integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg== + flatten@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" @@ -3457,15 +3779,6 @@ fs-extra@0.26.7: path-is-absolute "^1.0.0" rimraf "^2.2.8" -fs-extra@^4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94" - integrity sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg== - dependencies: - graceful-fs "^4.1.2" - jsonfile "^4.0.0" - universalify "^0.1.0" - fs-extra@^7.0.0: version "7.0.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" @@ -3475,6 +3788,15 @@ fs-extra@^7.0.0: jsonfile "^4.0.0" universalify "^0.1.0" +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs-minipass@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d" @@ -3574,6 +3896,11 @@ generate-object-property@^1.1.0: dependencies: is-property "^1.0.0" +gensync@^1.0.0-beta.1: + version "1.0.0-beta.1" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" + integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== + get-caller-file@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" @@ -3584,18 +3911,20 @@ get-caller-file@^2.0.1: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-stdin@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" - integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= - -get-stream@^4.0.0: +get-stream@^4.0.0, get-stream@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== dependencies: pump "^3.0.0" +get-stream@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.1.0.tgz#01203cdc92597f9b909067c3e656cc1f4d3c4dc9" + integrity sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw== + dependencies: + pump "^3.0.0" + get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" @@ -3646,7 +3975,7 @@ glob-parent@^3.0.0, glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" -glob-parent@~5.1.0: +glob-parent@^5.0.0, glob-parent@~5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.0.tgz#5f4c1d1e748d30cd73ad2944b3577a81b081e8c2" integrity sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw== @@ -3749,6 +4078,31 @@ glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.1.4, glob@^7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-agent@^2.0.2: + version "2.1.7" + resolved "https://registry.yarnpkg.com/global-agent/-/global-agent-2.1.7.tgz#12d7bc2b07cd862d0fa76b0f1b2c48cd5ffcf150" + integrity sha512-ooK7eqGYZku+LgnbfH/Iv0RJ74XfhrBZDlke1QSzcBt0bw1PmJcnRADPAQuFE+R45pKKDTynAr25SBasY2kvow== + dependencies: + boolean "^3.0.0" + core-js "^3.4.1" + es6-error "^4.1.1" + matcher "^2.0.0" + roarr "^2.14.5" + semver "^6.3.0" + serialize-error "^5.0.0" + global-modules@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" @@ -3785,6 +4139,16 @@ global-prefix@^3.0.0: kind-of "^6.0.2" which "^1.3.1" +global-tunnel-ng@^2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/global-tunnel-ng/-/global-tunnel-ng-2.7.1.tgz#d03b5102dfde3a69914f5ee7d86761ca35d57d8f" + integrity sha512-4s+DyciWBV0eK148wqXxcmVAbFVPqtc3sEtUE/GTQfuU80rySLcMhUmHKSHI7/LDj8q0gDYI1lIhRRB7ieRAqg== + dependencies: + encodeurl "^1.0.2" + lodash "^4.17.10" + npm-conf "^1.1.3" + tunnel "^0.0.6" + globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" @@ -3795,6 +4159,20 @@ globals@^11.7.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.10.0.tgz#1e09776dffda5e01816b3bb4077c8b59c24eaa50" integrity sha512-0GZF1RiPKU97IHUO5TORo9w1PwrH/NBPl+fS7oMLdaTRiYmYbwK4NWoZWrAdd0/abG9R2BU+OiwyQpTpE6pdfQ== +globals@^12.1.0: + version "12.3.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-12.3.0.tgz#1e564ee5c4dded2ab098b0f88f24702a3c56be13" + integrity sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw== + dependencies: + type-fest "^0.8.1" + +globalthis@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.1.tgz#40116f5d9c071f9e8fb0037654df1ab3a83b7ef9" + integrity sha512-mJPRTc/P39NH/iNG4mXa9aIhNymaQikTrnspeCa2ZuJ+mH2QN/rXwtX3XwKrHqWgUQFbNZKtHM105aHzJalElw== + dependencies: + define-properties "^1.1.3" + globby@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" @@ -3826,11 +4204,33 @@ glogg@^1.0.0: dependencies: sparkles "^1.0.0" +got@^9.6.0: + version "9.6.0" + resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" + integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== + dependencies: + "@sindresorhus/is" "^0.14.0" + "@szmarczak/http-timer" "^1.1.2" + cacheable-request "^6.0.0" + decompress-response "^3.3.0" + duplexer3 "^0.1.4" + get-stream "^4.1.0" + lowercase-keys "^1.0.1" + mimic-response "^1.0.1" + p-cancelable "^1.0.0" + to-readable-stream "^1.0.0" + url-parse-lax "^3.0.0" + graceful-fs@4.1.11, graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.6, graceful-fs@^4.1.9: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" integrity sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg= +graceful-fs@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" + integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== + growl@1.9.2: version "1.9.2" resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f" @@ -4050,18 +4450,6 @@ gulp-tsb@4.0.5: through "^2.3.6" vinyl "^2.1.0" -gulp-tslint@^8.1.3: - version "8.1.3" - resolved "https://registry.yarnpkg.com/gulp-tslint/-/gulp-tslint-8.1.3.tgz#a89ed144038ae861ee7bfea9528272d126a93da1" - integrity sha512-KEP350N5B9Jg6o6jnyCyKVBPemJePYpMsGfIQq0G0ErvY7tw4Lrfb/y3L4WRf7ek0OsaE8nnj86w+lcLXW8ovw== - dependencies: - "@types/fancy-log" "1.3.0" - chalk "2.3.1" - fancy-log "1.3.2" - map-stream "~0.0.7" - plugin-error "1.0.1" - through "~2.3.8" - gulp-untar@^0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/gulp-untar/-/gulp-untar-0.0.7.tgz#92067d79e0fa1e92d60562a100233a44a5aa08b4" @@ -4103,17 +4491,6 @@ gulplog@^1.0.0: dependencies: glogg "^1.0.0" -handlebars@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.2.tgz#b6b37c1ced0306b221e094fc7aca3ec23b131b67" - integrity sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw== - dependencies: - neo-async "^2.6.0" - optimist "^0.6.1" - source-map "^0.6.1" - optionalDependencies: - uglify-js "^3.1.4" - har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" @@ -4167,6 +4544,11 @@ has-flag@^3.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + has-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" @@ -4294,6 +4676,11 @@ html-comment-regex@^1.1.0: resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e" integrity sha1-ZouTd26q5V696POtRkswekljYl4= +html-escaper@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.0.tgz#71e87f931de3fe09e56661ab9a29aadec707b491" + integrity sha512-a4u9BeERWGu/S8JiWEAQcdrg9v4QArtP9keViQjGMdff20fBdd8waotXaNmODqBe6uZ3Nafi7K/ho4gCQHV3Ig== + "html-query-plan@git://github.com/anthonydresser/html-query-plan.git#2.6": version "2.5.0" resolved "git://github.com/anthonydresser/html-query-plan.git#c524feb824e4960897ad875a37af068376a2b4a3" @@ -4322,6 +4709,11 @@ htmlparser2@^3.10.0: inherits "^2.0.1" readable-stream "^3.1.1" +http-cache-semantics@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.0.3.tgz#495704773277eeef6e43f9ab2c2c7d259dda25c5" + integrity sha512-TcIMG3qeVLgDr1TEd2XvHaTnMPwYQUQMIBLy+5pLSDKYFc7UIqj39w8EGzZkaxoLv/l2K8HaI0t5AVA+YYgUew== + http-errors@1.6.2, http-errors@~1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" @@ -4455,13 +4847,6 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= -indent-string@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" - integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA= - dependencies: - repeating "^2.0.0" - indexes-of@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" @@ -4524,6 +4909,25 @@ inquirer@^6.1.0: strip-ansi "^5.0.0" through "^2.3.6" +inquirer@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.0.1.tgz#13f7980eedc73c689feff3994b109c4e799c6ebb" + integrity sha512-V1FFQ3TIO15det8PijPLFR9M9baSlnRs9nL7zWu1MNVA2T9YVl9ZbrHJhYs7e9X8jeMZ3lr2JH/rdHFgNCBdYw== + dependencies: + ansi-escapes "^4.2.1" + chalk "^2.4.2" + cli-cursor "^3.1.0" + cli-width "^2.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.15" + mute-stream "0.0.8" + run-async "^2.2.0" + rxjs "^6.5.3" + string-width "^4.1.0" + strip-ansi "^5.1.0" + through "^2.3.6" + interpret@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" @@ -4698,13 +5102,6 @@ is-extglob@^2.1.0, is-extglob@^2.1.1: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= -is-finite@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" - integrity sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko= - dependencies: - number-is-nan "^1.0.0" - is-fullwidth-code-point@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" @@ -4717,6 +5114,11 @@ is-fullwidth-code-point@^2.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + is-glob@^2.0.0, is-glob@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" @@ -4953,50 +5355,49 @@ isstream@~0.1.2: resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= -istanbul-lib-coverage@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49" - integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA== +istanbul-lib-coverage@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz#f5944a37c70b550b02a78a5c3b2055b280cec8ec" + integrity sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg== -istanbul-lib-instrument@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz#a5f63d91f0bbc0c3e479ef4c5de027335ec6d630" - integrity sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA== +istanbul-lib-instrument@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.0.tgz#53321a7970f076262fd3292c8f9b2e4ac544aae1" + integrity sha512-Nm4wVHdo7ZXSG30KjZ2Wl5SU/Bw7bDx1PdaiIFzEStdjs0H12mOTncn1GVYuqQSaZxpg87VGBRsVRPGD2cD1AQ== dependencies: - "@babel/generator" "^7.4.0" - "@babel/parser" "^7.4.3" - "@babel/template" "^7.4.0" - "@babel/traverse" "^7.4.3" - "@babel/types" "^7.4.0" - istanbul-lib-coverage "^2.0.5" - semver "^6.0.0" + "@babel/core" "^7.7.5" + "@babel/parser" "^7.7.5" + "@babel/template" "^7.7.4" + "@babel/traverse" "^7.7.4" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.0.0" + semver "^6.3.0" -istanbul-lib-report@^2.0.8: - version "2.0.8" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz#5a8113cd746d43c4889eba36ab10e7d50c9b4f33" - integrity sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ== +istanbul-lib-report@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" + integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== dependencies: - istanbul-lib-coverage "^2.0.5" - make-dir "^2.1.0" - supports-color "^6.1.0" + istanbul-lib-coverage "^3.0.0" + make-dir "^3.0.0" + supports-color "^7.1.0" -istanbul-lib-source-maps@^3.0.6: - version "3.0.6" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz#284997c48211752ec486253da97e3879defba8c8" - integrity sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw== +istanbul-lib-source-maps@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz#75743ce6d96bb86dc7ee4352cf6366a23f0b1ad9" + integrity sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg== dependencies: debug "^4.1.1" - istanbul-lib-coverage "^2.0.5" - make-dir "^2.1.0" - rimraf "^2.6.3" + istanbul-lib-coverage "^3.0.0" source-map "^0.6.1" -istanbul-reports@^2.2.6: - version "2.2.6" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-2.2.6.tgz#7b4f2660d82b29303a8fe6091f8ca4bf058da1af" - integrity sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA== +istanbul-reports@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.0.tgz#d4d16d035db99581b6194e119bbf36c963c5eb70" + integrity sha512-2osTcC8zcOSUkImzN2EWQta3Vdi4WjjKw99P2yWx5mLnigAM0Rd5uYFn1cf2i/Ois45GkNjaoTqc5CxgMSX80A== dependencies: - handlebars "^4.1.2" + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" istextorbinary@1.0.2: version "1.0.2" @@ -5056,7 +5457,7 @@ js-yaml@^3.12.0: argparse "^1.0.7" esprima "^4.0.0" -js-yaml@^3.13.0: +js-yaml@^3.13.1: version "3.13.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== @@ -5082,6 +5483,11 @@ jschardet@2.1.1: resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-2.1.1.tgz#af6f8fd0b3b0f5d46a8fd9614a4fce490575c184" integrity sha512-pA5qG9Zwm8CBpGlK/lo2GE9jPxwqRgMV7Lzc/1iaPccw6v4Rhj8Zg2BTyrdmHmxlJojnbLupLeRnaPLsq03x6Q== +jsdoctypeparser@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsdoctypeparser/-/jsdoctypeparser-6.1.0.tgz#acfb936c26300d98f1405cb03e20b06748e512a8" + integrity sha512-UCQBZ3xCUBv/PLfwKAJhp6jmGOSLFNKzrotXGNgbKhWvz27wPsCsVeP7gIcHPElQw2agBmynAitXqhxR58XAmA== + jsdom-no-contextify@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/jsdom-no-contextify/-/jsdom-no-contextify-3.1.0.tgz#0d8beaf610c2ff23894f54dfa7f89dd22fd0f7ab" @@ -5102,6 +5508,11 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== +json-buffer@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" + integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= + json-edm-parser@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/json-edm-parser/-/json-edm-parser-0.1.2.tgz#1e60b0fef1bc0af67bc0d146dfdde5486cd615b4" @@ -5141,7 +5552,7 @@ json-stable-stringify@^1.0.0: dependencies: jsonify "~0.0.0" -json-stringify-safe@~5.0.1: +json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= @@ -5158,6 +5569,13 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" +json5@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.1.tgz#81b6cb04e9ba496f1c7005d07b4368a2638f90b6" + integrity sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ== + dependencies: + minimist "^1.2.0" + jsonfile@^2.1.0: version "2.4.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" @@ -5210,6 +5628,13 @@ keytar@^4.11.0: nan "2.14.0" prebuild-install "5.3.0" +keyv@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" + integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== + dependencies: + json-buffer "3.0.0" + kind-of@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-1.1.0.tgz#140a3d2d41a36d2efcfa9377b62c24f8495a5c44" @@ -5458,12 +5883,32 @@ lodash.templatesettings@^4.0.0: dependencies: lodash._reinterpolate "~3.0.0" +lodash.unescape@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c" + integrity sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw= + lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5: +lodash@^4.15.0: + version "4.17.4" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" + integrity sha1-eCA6TRwyiuHYbcpkYONptX9AVa4= + +lodash@^4.17.10: + version "4.17.10" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" + integrity sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg== + +lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5: + version "4.17.11" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" + integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== + +lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -5488,13 +5933,15 @@ long@^3.2.0: resolved "https://registry.yarnpkg.com/long/-/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b" integrity sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s= -loud-rejection@^1.0.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" - integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= - dependencies: - currently-unhandled "^0.4.1" - signal-exit "^3.0.0" +lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" + integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== + +lowercase-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" + integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== lru-cache@2: version "2.7.3" @@ -5529,13 +5976,12 @@ make-dir@^1.0.0: dependencies: pify "^3.0.0" -make-dir@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" - integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== +make-dir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.0.0.tgz#1b5f39f6b9270ed33f9f054c5c0f84304989f801" + integrity sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw== dependencies: - pify "^4.0.1" - semver "^5.6.0" + semver "^6.0.0" make-iterator@^1.0.0: version "1.0.1" @@ -5561,12 +6007,7 @@ map-cache@^0.2.0, map-cache@^0.2.2: resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= -map-obj@^1.0.0, map-obj@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" - integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= - -map-stream@0.0.7, map-stream@~0.0.7: +map-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.0.7.tgz#8a1f07896d82b10926bd3744a2420009f88974a8" integrity sha1-ih8HiW2CsQkmvTdEokIACfiJdKg= @@ -5604,6 +6045,13 @@ matchdep@^2.0.0: resolve "^1.4.0" stack-trace "0.0.10" +matcher@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/matcher/-/matcher-2.1.0.tgz#64e1041c15b993e23b786f93320a7474bf833c28" + integrity sha512-o+nZr+vtJtgPNklyeUKkkH42OsK8WAfdgaJE2FNxcjLPg+5QbeEoT6vRj8Xq/iv18JlQ9cmKsEu0b94ixWf1YQ== + dependencies: + escape-string-regexp "^2.0.0" + math-expression-evaluator@^1.2.14: version "1.2.17" resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac" @@ -5653,22 +6101,6 @@ memory-fs@^0.4.0, memory-fs@^0.4.1, memory-fs@~0.4.1: errno "^0.1.3" readable-stream "^2.0.1" -meow@^3.1.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" - integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= - dependencies: - camelcase-keys "^2.0.0" - decamelize "^1.1.2" - loud-rejection "^1.0.0" - map-obj "^1.0.1" - minimist "^1.1.3" - normalize-package-data "^2.3.4" - object-assign "^4.0.1" - read-pkg-up "^1.0.1" - redent "^1.0.0" - trim-newlines "^1.0.0" - merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" @@ -5790,7 +6222,7 @@ mimic-fn@^1.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" integrity sha1-5md4PZLonb00KBi1IwudYqZyrRg= -mimic-fn@^2.0.0: +mimic-fn@^2.0.0, mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== @@ -5800,6 +6232,11 @@ mimic-response@^1.0.0: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.0.tgz#df3d3652a73fded6b9b0b24146e6fd052353458e" integrity sha1-3z02Uqc/3ta5sLJBRub9BSNTRY4= +mimic-response@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" + integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== + minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" @@ -5830,7 +6267,7 @@ minimist@0.0.8: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= -minimist@1.2.0, minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0: +minimist@1.2.0, minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= @@ -5984,6 +6421,11 @@ mute-stream@0.0.7, mute-stream@~0.0.4: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= +mute-stream@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== + nan@2.14.0, nan@^2.10.0, nan@^2.13.2, nan@^2.14.0: version "2.14.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" @@ -6021,10 +6463,10 @@ native-is-elevated@0.4.1: resolved "https://registry.yarnpkg.com/native-is-elevated/-/native-is-elevated-0.4.1.tgz#f6391aafb13441f5b949b39ae0b466b06e7f3986" integrity sha512-2vBXCXCXYKLDjP0WzrXs/AFjDb2njPR31EbGiZ1mR2fMJg211xClK1Xm19RXve35kvAL4dBKOFGCMIyc2+pPsw== -native-keymap@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/native-keymap/-/native-keymap-2.1.0.tgz#f3a92e647ac021fe552587b0020f8132efb03078" - integrity sha512-a5VYhjMqxe+HK5VzJM8yIcJOKkeuMSKYfmS0p7VEKSc7hM0F5IPsq7XO8KtwAgV8PJhfQVgAgyQmK8u/MQQ0aw== +native-keymap@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/native-keymap/-/native-keymap-2.1.1.tgz#8b6ba2d4e81b5fe44948ebe3ff88e3777b70d4d6" + integrity sha512-8ACRLXS9xMmRQVWK8YYp0zUSW4PjXuF2VGYu1kq0WYcND9yyr3BASyhopn+VXP3nDehS3Ct5FjDtsOiLEhBvmQ== native-watchdog@1.3.0: version "1.3.0" @@ -6055,11 +6497,6 @@ neo-async@^2.5.0: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.5.1.tgz#acb909e327b1e87ec9ef15f41b8a269512ad41ee" integrity sha512-3KL3fvuRkZ7s4IFOMfztb7zJp3QaVWnBeGoJlgB38XnCRPj/0tLzzLG5IB8NYOHbJ8g8UGrgZv44GLDk6CxTxA== -neo-async@^2.6.0: - version "2.6.1" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" - integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== - ng2-charts@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/ng2-charts/-/ng2-charts-1.6.0.tgz#108a2133ff62a8623895240fadbddbea2951f29d" @@ -6181,16 +6618,6 @@ normalize-package-data@^2.3.2: semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" -normalize-package-data@^2.3.4: - version "2.5.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" - integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== - dependencies: - hosted-git-info "^2.1.4" - resolve "^1.10.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" - normalize-path@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-1.0.0.tgz#32d0e472f91ff345701c15a8311018d3b0a90379" @@ -6228,6 +6655,11 @@ normalize-url@^1.4.0: query-string "^4.1.0" sort-keys "^1.0.0" +normalize-url@^4.1.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" + integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== + now-and-later@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/now-and-later/-/now-and-later-2.0.0.tgz#bc61cbb456d79cb32207ce47ca05136ff2e7d6ee" @@ -6240,6 +6672,14 @@ npm-bundled@^1.0.1: resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.3.tgz#7e71703d973af3370a9591bafe3a63aca0be2308" integrity sha512-ByQ3oJ/5ETLyglU2+8dBObvhfWXX8dtPZDMePCahptliFX2iIuhyEszyFk401PZUNQH20vvdW5MLjJxkwU80Ow== +npm-conf@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/npm-conf/-/npm-conf-1.1.3.tgz#256cc47bd0e218c259c4e9550bf413bc2192aff9" + integrity sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw== + dependencies: + config-chain "^1.1.11" + pify "^3.0.0" + npm-packlist@^1.1.6: version "1.1.11" resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.11.tgz#84e8c683cbe7867d34b1d357d893ce29e28a02de" @@ -6272,19 +6712,6 @@ nth-check@~1.0.1: dependencies: boolbase "~1.0.0" -nugget@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/nugget/-/nugget-2.0.1.tgz#201095a487e1ad36081b3432fa3cada4f8d071b0" - integrity sha1-IBCVpIfhrTYIGzQy+jytpPjQcbA= - dependencies: - debug "^2.1.3" - minimist "^1.1.0" - pretty-bytes "^1.0.2" - progress-stream "^1.1.0" - request "^2.45.0" - single-line-log "^1.1.2" - throttleit "0.0.2" - num2fraction@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" @@ -6361,6 +6788,11 @@ object.defaults@^1.0.0, object.defaults@^1.1.0: for-own "^1.0.0" isobject "^3.0.0" +object.entries-ponyfill@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object.entries-ponyfill/-/object.entries-ponyfill-1.0.1.tgz#29abdf77cbfbd26566dd1aa24e9d88f65433d256" + integrity sha1-Kavfd8v70mVm3RqiTp2I9lQz0lY= + object.map@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/object.map/-/object.map-1.0.1.tgz#cf83e59dc8fcc0ad5f4250e1f78b3b81bd801d37" @@ -6413,6 +6845,13 @@ onetime@^2.0.0: dependencies: mimic-fn "^1.0.0" +onetime@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.0.tgz#fff0f3c91617fe62bb50189636e99ac8a6df7be5" + integrity sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q== + dependencies: + mimic-fn "^2.1.0" + onigasm-umd@2.2.5: version "2.2.5" resolved "https://registry.yarnpkg.com/onigasm-umd/-/onigasm-umd-2.2.5.tgz#f104247334a543accd3f8d641a4d99b3d908d6a1" @@ -6459,6 +6898,18 @@ optionator@^0.8.2: type-check "~0.3.2" wordwrap "~1.0.0" +optionator@^0.8.3: + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + ordered-read-streams@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-0.3.0.tgz#7137e69b3298bb342247a1bbee3881c80e2fd78b" @@ -6528,6 +6979,11 @@ p-all@^1.0.0: dependencies: p-map "^1.0.0" +p-cancelable@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" + integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== + p-defer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" @@ -6813,11 +7269,6 @@ pify@^3.0.0: resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= -pify@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" - integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== - pinkie-promise@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" @@ -6869,7 +7320,7 @@ plugin-error@0.1.2, plugin-error@^0.1.2: arr-union "^2.0.1" extend-shallow "^1.1.2" -plugin-error@1.0.1, plugin-error@^1.0.1: +plugin-error@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-1.0.1.tgz#77016bd8919d0ac377fdcdd0322328953ca5781c" integrity sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA== @@ -7165,19 +7616,16 @@ prepend-http@^1.0.0: resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= +prepend-http@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" + integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= + preserve@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" integrity sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks= -pretty-bytes@^1.0.2: - version "1.0.4" - resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-1.0.4.tgz#0a22e8210609ad35542f8c8d5d2159aff0751c84" - integrity sha1-CiLoIQYJrTVUL4yNXSFZr/B1HIQ= - dependencies: - get-stdin "^4.0.1" - meow "^3.1.0" - pretty-hrtime@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" @@ -7206,14 +7654,6 @@ process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= -progress-stream@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/progress-stream/-/progress-stream-1.2.0.tgz#2cd3cfea33ba3a89c9c121ec3347abe9ab125f77" - integrity sha1-LNPP6jO6OonJwSHsM0er6asSX3c= - dependencies: - speedometer "~0.1.2" - through2 "~0.2.3" - progress@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" @@ -7421,7 +7861,7 @@ raw-body@2.3.2: iconv-lite "0.4.19" unpipe "1.0.0" -rc@^1.2.1, rc@^1.2.7: +rc@^1.2.7: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== @@ -7497,7 +7937,7 @@ read@^1.0.7: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@^1.1.8, readable-stream@~1.1.9: +readable-stream@^1.1.8: version "1.1.14" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= @@ -7574,14 +8014,6 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" -redent@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" - integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94= - dependencies: - indent-string "^2.1.0" - strip-indent "^1.0.1" - reduce-css-calc@^1.2.6: version "1.3.0" resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716" @@ -7624,6 +8056,11 @@ regexpp@^2.0.1: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== +regextras@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/regextras/-/regextras-0.7.0.tgz#2298bef8cfb92b1b7e3b9b12aa8f69547b7d71e4" + integrity sha512-ds+fL+Vhl918gbAUb0k2gVKbTZLsg84Re3DI6p85Et0U0tYME3hyW4nMK8Px4dtDaBA2qNjvG5uWyW7eK5gfmw== + remove-bom-buffer@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz#c2bf1e377520d324f623892e33c10cac2c252b53" @@ -7661,13 +8098,6 @@ repeat-string@^1.6.1: resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= -repeating@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" - integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= - dependencies: - is-finite "^1.0.0" - replace-ext@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924" @@ -7750,7 +8180,7 @@ request@2.79.0: tunnel-agent "^0.6.0" uuid "^3.1.0" -request@^2.45.0, request@^2.86.0, request@^2.88.0: +request@^2.86.0, request@^2.88.0: version "2.88.0" resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== @@ -7837,17 +8267,17 @@ resolve-url@^0.2.1: resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= -resolve@^1.1.6, resolve@^1.1.7, resolve@^1.3.2: +resolve@^1.1.6, resolve@^1.1.7: version "1.5.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36" integrity sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw== dependencies: path-parse "^1.0.5" -resolve@^1.10.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.13.1.tgz#be0aa4c06acd53083505abb35f4d66932ab35d16" - integrity sha512-CxqObCX8K8YtAhOBRg+lrcdn+LK+WYOS8tSjqSFbjtrI5PnS63QPhZl4+yKfrU9tdsbMu9Anr/amegT87M9Z6w== +resolve@^1.3.2: + version "1.14.2" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.14.2.tgz#dbf31d0fa98b1f29aa5169783b9c290cb865fea2" + integrity sha512-EjlOBLBO1kxsUxsKjLt7TAECyKW6fOh1VRkykQkKGzcBbjjPIxBqGh0jf7GJ3k/f5mxMqW3htMD3WdTUVtW8HQ== dependencies: path-parse "^1.0.6" @@ -7858,6 +8288,13 @@ resolve@^1.4.0: dependencies: path-parse "^1.0.6" +responselike@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" + integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= + dependencies: + lowercase-keys "^1.0.0" + restore-cursor@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" @@ -7866,12 +8303,20 @@ restore-cursor@^2.0.0: onetime "^2.0.0" signal-exit "^3.0.2" +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + ret@~0.1.10: version "0.1.15" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== -rimraf@2, rimraf@^2.6.3: +rimraf@2, rimraf@2.6.3: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== @@ -7905,6 +8350,18 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" +roarr@^2.14.5: + version "2.14.6" + resolved "https://registry.yarnpkg.com/roarr/-/roarr-2.14.6.tgz#cebe8ad7ecbfd15bfa37b02dacf00809dd633912" + integrity sha512-qjbw0BEesKA+3XFBPt+KVe1PC/Z6ShfJ4wPlx2XifqH5h2Lj8/KQT5XJTsy3n1Es5kai+BwKALaECW3F70B1cg== + dependencies: + boolean "^3.0.0" + detect-node "^2.0.4" + globalthis "^1.0.0" + json-stringify-safe "^5.0.1" + semver-compare "^1.0.0" + sprintf-js "^1.1.2" + run-async@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" @@ -7933,6 +8390,13 @@ rxjs@^6.4.0: dependencies: tslib "^1.9.0" +rxjs@^6.5.3: + version "6.5.3" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.3.tgz#510e26317f4db91a7eb1de77d9dd9ba0a4899a3a" + integrity sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA== + dependencies: + tslib "^1.9.0" + safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" @@ -7965,6 +8429,13 @@ samsam@~1.1: resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.3.tgz#9f5087419b4d091f232571e7fa52e90b0f552621" integrity sha1-n1CHQZtNCR8jJXHn+lLpCw9VJiE= +sanitize-filename@^1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.3.tgz#755ebd752045931977e30b2025d340d7c9090378" + integrity sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg== + dependencies: + truncate-utf8-bytes "^1.0.0" + sanitize-html@^1.19.1: version "1.20.0" resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.20.0.tgz#9a602beb1c9faf960fb31f9890f61911cc4d9156" @@ -7999,6 +8470,11 @@ schema-utils@^0.4.4, schema-utils@^0.4.5: ajv "^6.1.0" ajv-keywords "^3.1.0" +semver-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" + integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= + semver-greatest-satisfied-range@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz#13e8c2658ab9691cb0cd71093240280d36f77a5b" @@ -8036,6 +8512,11 @@ semver@^6.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.2.0.tgz#4d813d9590aaf8a9192693d6c85b9344de5901db" integrity sha512-jdFC1VdUGT/2Scgbimf7FSx9iJLXoqfglSF+gJeuNWVpiE37OIbc1jywR/GJyFdz3mnkz2/id0L0J/cr0izR5A== +semver@^6.1.2, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + send@0.16.1: version "0.16.1" resolved "https://registry.yarnpkg.com/send/-/send-0.16.1.tgz#a70e1ca21d1382c11d0d9f6231deb281080d7ab3" @@ -8055,6 +8536,13 @@ send@0.16.1: range-parser "~1.2.0" statuses "~1.3.1" +serialize-error@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-5.0.0.tgz#a7ebbcdb03a5d71a6ed8461ffe0fc1a1afed62ac" + integrity sha512-/VtpuyzYf82mHYTtI4QKtwHa79vAdU5OQpNPAmE/0UDdlGT0ZxHwC+J6gXkw29wwoVI8fMPsfcVHOwXtUQYYQA== + dependencies: + type-fest "^0.8.0" + serialize-javascript@^1.4.0: version "1.5.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.5.0.tgz#1aa336162c88a890ddad5384baebc93a655161fe" @@ -8164,13 +8652,6 @@ simple-get@^2.7.0: once "^1.3.1" simple-concat "^1.0.0" -single-line-log@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/single-line-log/-/single-line-log-1.1.2.tgz#c2f83f273a3e1a16edb0995661da0ed5ef033364" - integrity sha1-wvg/Jzo+GhbtsJlWYdoO1e8DM2Q= - dependencies: - string-width "^1.0.1" - sinon@^1.17.2: version "1.17.7" resolved "https://registry.yarnpkg.com/sinon/-/sinon-1.17.7.tgz#4542a4f49ba0c45c05eb2e9dd9d203e2b8efe0bf" @@ -8186,7 +8667,7 @@ slash@^1.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU= -slice-ansi@^2.0.0: +slice-ansi@^2.0.0, slice-ansi@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== @@ -8330,6 +8811,19 @@ spdx-correct@~1.0.0: dependencies: spdx-license-ids "^1.0.2" +spdx-exceptions@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977" + integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA== + +spdx-expression-parse@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" + integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + spdx-expression-parse@~1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" @@ -8340,10 +8834,10 @@ spdx-license-ids@^1.0.2: resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" integrity sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc= -speedometer@~0.1.2: - version "0.1.4" - resolved "https://registry.yarnpkg.com/speedometer/-/speedometer-0.1.4.tgz#9876dbd2a169d3115402d48e6ea6329c8816a50d" - integrity sha1-mHbb0qFp0xFUAtSObqYynIgWpQ0= +spdx-license-ids@^3.0.0: + version "3.0.5" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" + integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" @@ -8366,6 +8860,11 @@ split@^1.0.1: dependencies: through "2" +sprintf-js@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673" + integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug== + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -8524,6 +9023,15 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" +string-width@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + string_decoder@^1.0.0, string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -8588,6 +9096,13 @@ strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + strip-bom-stream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-bom-stream/-/strip-bom-stream-1.0.0.tgz#e7144398577d51a6bed0fa1994fa05f43fd988ee" @@ -8608,29 +9123,27 @@ strip-eof@^1.0.0: resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= -strip-indent@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" - integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI= - dependencies: - get-stdin "^4.0.1" - strip-json-comments@^2.0.1, strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= +strip-json-comments@^3.0.1: + version "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== + sudo-prompt@9.1.1: version "9.1.1" resolved "https://registry.yarnpkg.com/sudo-prompt/-/sudo-prompt-9.1.1.tgz#73853d729770392caec029e2470db9c221754db0" integrity sha512-es33J1g2HjMpyAhz8lOR+ICmXXAqTuKbuXuUWLhOLew20oN9oUCgCJx615U/v7aioZg7IX5lIh9x34vwneu4pA== -sumchecker@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-2.0.2.tgz#0f42c10e5d05da5d42eea3e56c3399a37d6c5b3e" - integrity sha1-D0LBDl0F2l1C7qPlbDOZo31sWz4= +sumchecker@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-3.0.1.tgz#6377e996795abb0b6d348e9b3e1dfb24345a8e42" + integrity sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg== dependencies: - debug "^2.2.0" + debug "^4.1.0" supports-color@1.2.0: version "1.2.0" @@ -8663,13 +9176,6 @@ supports-color@^4.0.0: dependencies: has-flag "^2.0.0" -supports-color@^5.2.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - supports-color@^5.3.0: version "5.4.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" @@ -8677,6 +9183,13 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" +supports-color@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + sver-compat@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/sver-compat/-/sver-compat-1.5.0.tgz#3cf87dfeb4d07b4a3f14827bc186b3fd0c645cd8" @@ -8713,6 +9226,16 @@ table@^5.0.2: slice-ansi "^2.0.0" string-width "^2.1.1" +table@^5.2.3: + version "5.4.6" + resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" + integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== + dependencies: + ajv "^6.10.2" + lodash "^4.17.14" + slice-ansi "^2.1.0" + string-width "^3.0.0" + tapable@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.0.0.tgz#cbb639d9002eed9c6b5975eb20598d7936f1f9f2" @@ -8798,11 +9321,6 @@ textextensions@~1.0.0: resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-1.0.2.tgz#65486393ee1f2bb039a60cbba05b0b68bd9501d2" integrity sha1-ZUhjk+4fK7A5pgy7oFsLaL2VAdI= -throttleit@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-0.0.2.tgz#cfedf88e60c00dd9697b61fdd2a8343a9b680eaf" - integrity sha1-z+34jmDADdlpe2H90qg0OptoDq8= - through2-filter@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-2.0.0.tgz#60bc55a0dacb76085db1f9dae99ab43f83d622ec" @@ -8835,14 +9353,6 @@ through2@^3.0.0: readable-stream "2 || 3" xtend "~4.0.1" -through2@~0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/through2/-/through2-0.2.3.tgz#eb3284da4ea311b6cc8ace3653748a52abf25a3f" - integrity sha1-6zKE2k6jEbbMis42U3SKUqvyWj8= - dependencies: - readable-stream "~1.1.9" - xtend "~2.1.1" - through2@~0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/through2/-/through2-0.4.2.tgz#dbf5866031151ec8352bb6c4db64a2292a840b9b" @@ -8851,7 +9361,7 @@ through2@~0.4.0: readable-stream "~1.0.17" xtend "~2.1.1" -through@2, through@^2.3.6, through@^2.3.8, through@~2.3, through@~2.3.1, through@~2.3.4, through@~2.3.8: +through@2, through@^2.3.6, through@^2.3.8, through@~2.3, through@~2.3.1, through@~2.3.4: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= @@ -8931,6 +9441,11 @@ to-object-path@^0.3.0: dependencies: kind-of "^3.0.2" +to-readable-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" + integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== + to-regex-range@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" @@ -8997,15 +9512,12 @@ tough-cookie@~2.4.3: resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" integrity sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk= -trim-newlines@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" - integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= - -trim-right@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" - integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= +truncate-utf8-bytes@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b" + integrity sha1-QFkjkJWS1W94pYGENLC3hInKXys= + dependencies: + utf8-byte-length "^1.0.1" ts-loader@^4.4.2: version "4.4.2" @@ -9018,53 +9530,15 @@ ts-loader@^4.4.2: micromatch "^3.1.4" semver "^5.0.1" -tslib@^1.8.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.0.tgz#e37a86fda8cbbaf23a057f473c9f4dc64e5fc2e8" - integrity sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ== - tslib@^1.8.1, tslib@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== -tslint-microsoft-contrib@^6.0.0: - version "6.1.1" - resolved "https://registry.yarnpkg.com/tslint-microsoft-contrib/-/tslint-microsoft-contrib-6.1.1.tgz#1de9b5c2867f6cec762bab9d8e1619f2b8eb59fc" - integrity sha512-u6tK+tgt8Z1YRJxe4kpWWEx/6FTFxdga50+osnANifsfC7BMzh8c/t/XbNntTRiwMfpHkQ9xtUjizCDLxN1Yeg== - dependencies: - tsutils "^2.27.2 <2.29.0" - -tslint@^5.16.0: - version "5.16.0" - resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.16.0.tgz#ae61f9c5a98d295b9a4f4553b1b1e831c1984d67" - integrity sha512-UxG2yNxJ5pgGwmMzPMYh/CCnCnh0HfPgtlVRDs1ykZklufFBL1ZoTlWFRz2NQjcoEiDoRp+JyT0lhBbbH/obyA== - dependencies: - "@babel/code-frame" "^7.0.0" - builtin-modules "^1.1.1" - chalk "^2.3.0" - commander "^2.12.1" - diff "^3.2.0" - glob "^7.1.1" - js-yaml "^3.13.0" - minimatch "^3.0.4" - mkdirp "^0.5.1" - resolve "^1.3.2" - semver "^5.3.0" - tslib "^1.8.0" - tsutils "^2.29.0" - -"tsutils@^2.27.2 <2.29.0": - version "2.28.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.28.0.tgz#6bd71e160828f9d019b6f4e844742228f85169a1" - integrity sha512-bh5nAtW0tuhvOJnx1GLRn5ScraRLICGyJV5wJhtRWOLsxW70Kk5tZtpK3O/hW6LDnqKS9mlUMPZj9fEMJ0gxqA== - dependencies: - tslib "^1.8.1" - -tsutils@^2.29.0: - version "2.29.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99" - integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA== +tsutils@^3.17.1: + version "3.17.1" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" + integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g== dependencies: tslib "^1.8.1" @@ -9090,6 +9564,11 @@ tunnel@0.0.4: resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.4.tgz#2d3785a158c174c9a16dc2c046ec5fc5f1742213" integrity sha1-LTeFoVjBdMmhbcLARuxfxfF0IhM= +tunnel@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" + integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" @@ -9102,6 +9581,11 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" +type-fest@^0.8.0, type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + type-is@~1.6.15: version "1.6.15" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" @@ -9138,10 +9622,10 @@ typescript-formatter@7.1.0: commandpost "^1.0.0" editorconfig "^0.15.0" -typescript@3.7.3: - version "3.7.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.3.tgz#b36840668a16458a7025b9eabfad11b66ab85c69" - integrity sha512-Mcr/Qk7hXqFBXMN7p7Lusj1ktCBydylfQM/FZCk5glCNQJrCUKPkMHdo9R0MTFWsC/4kPFvDS0fDPvukfCkFsw== +typescript@3.8.0-beta: + version "3.8.0-beta" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.0-beta.tgz#acdcaf9f24c7e20b1ff0a6329d1e8d63691e2e13" + integrity sha512-mQEmQUJg0CQBhf/GSVnGscKv/jrKsrLxE01AhdjYmBNoXX2Iah3i38ufxXByXacK6Fc5Nr9oMz7MjpjgddiknA== typescript@^2.6.2: version "2.6.2" @@ -9161,14 +9645,6 @@ uglify-es@^3.3.4: commander "~2.13.0" source-map "~0.6.1" -uglify-js@^3.1.4: - version "3.6.0" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.6.0.tgz#704681345c53a8b2079fb6cec294b05ead242ff5" - integrity sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg== - dependencies: - commander "~2.20.0" - source-map "~0.6.1" - uglifyjs-webpack-plugin@^1.2.4: version "1.2.7" resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.7.tgz#57638dd99c853a1ebfe9d97b42160a8a507f9d00" @@ -9312,6 +9788,13 @@ url-join@^1.1.0: resolved "https://registry.yarnpkg.com/url-join/-/url-join-1.1.0.tgz#741c6c2f4596c4830d6718460920d0c92202dc78" integrity sha1-dBxsL0WWxIMNZxhGCSDQySIC3Hg= +url-parse-lax@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" + integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww= + dependencies: + prepend-http "^2.0.0" + url@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" @@ -9325,6 +9808,11 @@ use@^3.1.0: resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== +utf8-byte-length@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61" + integrity sha1-9F8VDExm7uloGGUFq5P8u4rWv2E= + util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -9364,6 +9852,11 @@ v8-compile-cache@2.0.3: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe" integrity sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w== +v8-compile-cache@^2.0.3: + version "2.1.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e" + integrity sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g== + v8-inspect-profiler@^0.0.20: version "0.0.20" resolved "https://registry.yarnpkg.com/v8-inspect-profiler/-/v8-inspect-profiler-0.0.20.tgz#f7ad0f8178dcea2f1504334e8844ef38181792ab" @@ -9797,6 +10290,11 @@ windows-process-tree@0.2.4: dependencies: nan "^2.13.2" +word-wrap@~1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + wordwrap@~0.0.2: version "0.0.3" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" @@ -9836,6 +10334,13 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= +write@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" + integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== + dependencies: + mkdirp "^0.5.1" + write@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" @@ -9919,15 +10424,15 @@ xterm-addon-web-links@0.2.1: resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.2.1.tgz#6d1f2ce613e09870badf17615e7a1170a31542b2" integrity sha512-2KnHtiq0IG7hfwv3jw2/jQeH1RBk2d5CH4zvgwQe00rLofSJqSfgnJ7gwowxxpGHrpbPr6Lv4AmH/joaNw2+HQ== -xterm-addon-webgl@0.4.0-beta.11: - version "0.4.0-beta.11" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.4.0-beta.11.tgz#0e4a7242e2353cf74aba55e5a5bdc0b4ec87ad10" - integrity sha512-AteDxm1RFy1WnjY9r5iJSETozLebvUkR+jextdZk/ASsK21vsYK0DuVWwRI8afgiN2hUVhxcxuHEJUOV+CJDQA== +xterm-addon-webgl@0.5.0-beta.7: + version "0.5.0-beta.7" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.5.0-beta.7.tgz#b7b95a362e942ad6f86fa286d7b7bd8ee3e7cf67" + integrity sha512-v6aCvhm1C6mvaurGwUYQfyhb2cAUyuVnzf3Ob/hy5ebtyzUj4wW0N9NbqDEJk67UeMi1lV2xZqrO5gNeTpVqFA== -xterm@4.3.0-beta.28.vscode.1: - version "4.3.0-beta.28.vscode.1" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.3.0-beta.28.vscode.1.tgz#89b85398b5801708e833d08bf92b2124fa128943" - integrity sha512-JNHNZyDtAWnybJTrenPzD6g/yXpHOvPqmjau91Up4onRbjQYMSNlth17SqaES68DKn/+4kcIl2c/RG5SXJjvGw== +xterm@4.4.0-beta.15: + version "4.4.0-beta.15" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.4.0-beta.15.tgz#5897bf79d29d1a2496ccd54665aded28c341b1cc" + integrity sha512-Dvz1CMCYKeoxPF7uIDznbRgUA2Mct49Bq93K2nnrDU0pDMM3Sf1t9fkEyz59wxSx5XEHVdLS80jywsz4sjXBjQ== y18n@^3.2.1: version "3.2.1"